Compare commits

..

25 Commits

Author SHA1 Message Date
Elias Nahum
9ba81fbf96 MM-15216 Fix crash when cookie does not set expiration date on iOS 2019-04-18 18:25:53 -04:00
Elias Nahum
689ef9cb8b MM-15215 fix crash caused by malformed post textbox localize string 2019-04-18 18:25:46 -04:00
Elias Nahum
f13df74c44 Bump app version number to 1.18.1 2019-04-18 18:25:39 -04:00
Elias Nahum
812c20934d Bump app build number to 187 2019-04-18 18:25:32 -04:00
Saturnino Abril
7590bb3063 Bump app build number to 186 (#2716) 2019-04-16 11:58:07 +08:00
Miguel Alatzar
69a2c58f5e Ensure the correct value for channelIsLoading is used (#2712) 2019-04-15 10:54:23 -07:00
Saturnino Abril
7efb044aa9 Bump app build number to 185 (#2708) 2019-04-10 16:57:52 +08:00
Elias Nahum
25673ff7e0 MM-14959 iOS Share Extension channel search open by default (#2698) 2019-04-09 14:39:35 -04:00
JoramWilander
78b23ae37e Update mattermost-redux 2019-04-09 13:27:09 -04:00
Elias Nahum
8ef6b35369 translations PR 20190408 (#2704) 2019-04-09 07:52:21 -04:00
Miguel Alatzar
385a081f78 Set canFlag to false for system messages (#2705) 2019-04-09 11:16:47 +08:00
Elias Nahum
0377249592 MM-14960 Use the correct icon set for channel types (#2699) 2019-04-08 10:51:10 -07:00
Miguel Alatzar
caac14907e [MM-14830] Check for null pushNotification prior to calling sendNotificationScheduled on it (#2702)
* Update react-native-notifications dependency commit hash

* Add package-lock.json
2019-04-08 08:33:33 -04:00
Miguel Alatzar
6fef6d6b92 [MM-14899] Support sharing of screenshots via the iOS share extension (#2697)
* Handle NSItemProvider item of type UIImage

* Create screenshot temp file with UploadSessionManager
2019-04-08 05:31:11 +08:00
Miguel Alatzar
64337b4851 Remove package name from push notification content (#2700) 2019-04-08 05:25:30 +08:00
Miguel Alatzar
713dd4e578 Avoid force casting items to URL (#2696) 2019-04-05 02:59:45 +08:00
Saturnino Abril
41ddeef2f7 Bump app build number to 184 (#2695) 2019-04-05 00:22:43 +08:00
Saturnino Abril
00e05c5e8f update commit of commonmark.js that fixes "blank spaces break markdown table" (#2694) 2019-04-05 00:14:54 +08:00
Daniel Schalla
a74fabcc98 Add CSRF Header in File Upload and Profile Image Set Operations (#2686) 2019-04-04 10:44:11 -04:00
Miguel Alatzar
bb9f96f409 [MM-14871] Fix extraction of sender name for push notifications (#2691)
* Return empty string for sender name when not found
2019-04-03 18:42:14 -07:00
Elias Nahum
3adec36c95 translations PR 20190401 (#2687) 2019-04-03 18:31:54 -03:00
Miguel Alatzar
4c690b5578 [MM-14866] Fix app crash due to parseInt call on null and display test notification message (#2690)
* Call setNumber on notification only if badge is not null

* Move setNumber call

* Add bundle to empty list
2019-04-03 10:15:34 -07:00
Dan Maas
39129fc6c4 Update NOTICE.txt (#2685)
- Misc homepage and project info updates
2019-04-03 09:47:41 +02:00
Christopher Speller
370fa9b952 MM-13618 Adding bot tags. (#2669)
* Adding bot tags.

* Snapshot update.

* Moving bot tag to own component.

* Snapshots.

* Adding missing bot tags, fixing bot profile, allowing tap of bot username to open profile.

* Snapshots and tests.
2019-03-26 17:01:17 -07:00
Harshil Sharma
52e379ae51 #MI-372 updated mattermost-redux commit ID to use the latest changes (#2670)
* #MI-372 updated mattermost-redux commit ID to use the latest changes

* Updated commit ID in package lock as well
2019-03-26 18:38:56 -03:00
501 changed files with 9120 additions and 16279 deletions

View File

@@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[version]
^0.92.0
^0.78.0

View File

@@ -1,85 +1,12 @@
# Mattermost Mobile Apps Changelog
## 1.19.0 Release
- Release Date: May 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where Android managed config was lost on the thread view.
- Fixed an issue where contents of ephemeral posts did not display on the mobile app.
- Fixed a few mobile app crash / fatal error issues.
- Fixed an issue with an expanding animation when tapping on Jump to Channel in the channel list.
- Fixed an issue on iOS where animated custom emoji weren't animated.
- Fixed an issue on iOS where users were unable to create channel name of 2 characters.
- Fixed an issue on iOS where emoji appeared too close, with uneven spacing, and too small in the info modal.
- Added an error handler when sharing text that was over server's maximum post size with the iOS Share Extension.
- Fixed an issue where users could upload a GIF as a profile image.
### Known Issues
- Buttons inside ephemeral posts are not clickable / functional on the mobile app.
## 1.18.1 Release
- Release Date: April 18, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed a crash issue caused by a malformed post textbox localize string.
- Fixed an issue where iOS crashed when trying to log in using SSO and the SSO provider set a cookie without an expiration date.
## 1.18.0 Release
- Release Date: April 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
- ``Bot`` tags were added for bot accounts feature in server v5.10 and mobile v1.18, meaning that mobile v1.17 and earlier don't support the tags.
### Highlights
- Added support for Office365 single sign-on (SSO).
- Added support for Integrated Windows Authentication (IWA).
### Improvements
- Added the ability for channel links to open inside the app.
- Added ability for emojis and hyperlinks to render in the message attachment title.
- Added Chinese support for words that trigger mentions.
- Added a setting to the system console to change the minimum length of hashtags.
- Added a reply option to long press context menu.
### Bug Fixes
- Fixed an issue where blank spaces broke markdown tables.
- Fixed an issue where deactivated users appeared on "Add Members" modal but not on the search results.
- Fixed an issue on Android where extra text in the search box appeared after using the autocomplete drop-down.
- Fixed an issue with multiple text entries when typing with Shift+Letter on Android.
- Fixed an issue where push notifications badges did not always clear when read on another device.
- Fixed an issue where opening a single or group notification did not take the user into the channel where the notification came from.
- Fixed an issue where timezone did not automatically update on Android when travelling to another timezone.
- Fixed an issue where the user mention autocomplete drop-down was case sensitive.
- Fixed an issue where system admininistrators were able to see the full long press menu when long pressing a system message.
- Fixed an issue where users were not able to unflag posts from "Flagged Posts" when opened from a read-only channel.
- Fixed an issue where users were unable to create channel names of 2 byte characters.
### Known Issues
- Content for ephemeral messages is not displayed on Mattermost Mobile Apps.
## 1.17.0 Release
- Release Date: March 20, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
- If you are using an EMM provider via AppConfig, make sure to add two new settings, `useVPN` and `timeoutVPN`, to your AppConfig file. The settings were added for EMM connections using VPN on-demand - one to indicate if every request should wait for the VPN connection to be established, and another to set the timeout in seconds. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Fixed support for EMM connections using VPN on-demand to indicate that every request should wait for the VPN connection to be establish and to set the value in seconds for the timeout. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.

View File

@@ -2,4 +2,33 @@
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/).
### Review Process for this Repo
After following the steps in the [Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html), submitted pull requests go through the review process outlined below. We aim to start reviewing pull requests in this repo the week they are submitted, but the length of time to complete the process will vary depending on the pull request.
The one exception may be around release time, where the review process may take longer as the team focuses on our [release process](https://docs.mattermost.com/process/release-process.html).
#### `Stage 1: PM Review`
A Product Manager will review the pull request to make sure it:
1. Fits with our product roadmap
2. Works as expected
3. Meets UX guidelines
This step is sometimes skipped for bugs or small improvements with a ticket, but always happens for new features or pull requests without a related ticket.
The Product Manager may come back with some bugs or UI improvements to fix before the pull request moves on to the next stage.
#### `Stage 2: Dev Review`
Two developers will review the pull request and either give feedback or `+1` the PR.
Any comments will need to be addressed before the pull request moves on to the last stage.
- PRs that do not follow Style Guides cannot be merged
#### `Stage 3: Ready to Merge`
The review process is complete, and the pull request will be merged.

View File

@@ -76,11 +76,12 @@ post-install:
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
@# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
@# Need to copy custom RNCWebViewManager.java and RNCWEKWebView.m that implements IWA support for the WebView to avoid forking the lib
@cp ./native_modules/RNCWebViewManager.java node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
@cp ./native_modules/RNCWKWebView.m node_modules/react-native-webview/ios/RNCWKWebView.m
@# Need to copy custom RNCNetInfo.m that checks for internet connectivity instead of reaching a host by default
@cp ./native_modules/RNCNetInfo.m node_modules/@react-native-community/netinfo/ios/RNCNetInfo.m
# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@@ -202,7 +203,6 @@ unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.ipa os_type:iOS
@rm -rf build-ios/
$(call stop_packager)
@@ -215,7 +215,6 @@ ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version o
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
@rm -rf build-ios/
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-simulator-x86_64.app.zip os_type:iOS
$(call stop_packager)
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
@@ -223,7 +222,6 @@ unsigned-android: stop pre-build check-style prepare-android-build ## Build an u
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/unsigned/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.apk os_type:Android
$(call stop_packager)
test: | pre-run check-style ## Runs tests
@@ -242,7 +240,9 @@ can-build-pr:
fi
i18n-extract: ## Extract strings for translation from the source code
npm run mmjstool -- i18n extract-mobile
@[[ -d $(MM_UTILITIES_DIR) ]] || echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
@[[ -d $(MM_UTILITIES_DIR) ]] && cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-mobile
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:

View File

@@ -7,6 +7,42 @@ NOTICES:
This document includes a list of open source components used in Mattermost Mobile, including those that have been modified.
--------
## @babel/polyfill
This product contains 'polyfill' by Sebastian McKenzie.
Provides polyfills necessary for a full ES2015+ environment
* HOMEPAGE:
* https://babeljs.io/
* LICENSE: MIT
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other 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.
---
## @babel/runtime
This product contains 'runtime' by Sebastian McKenzie.
@@ -43,76 +79,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## @react-native-community/async-storage
This product contains 'async-storage' by Krzysztof Borowy.
Asynchronous, persistent, key-value storage system for React Native.
* HOMEPAGE:
* https://github.com/react-native-community/react-native-async-storage#readme
* LICENSE: MIT
MIT License
Copyright (c) 2015-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-community/netinfo
This product contains 'netinfo' by Matt Oakes.
React Native Network Info API for iOS & Android
* HOMEPAGE:
* https://github.com/react-native-community/react-native-netinfo#readme
* LICENSE: MIT
MIT License
Copyright (c) 2015-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.
---
## analytics-react-native
This product contains 'analytics-react-native' by Javier Alvarez.
@@ -759,7 +725,7 @@ Common code (API client, Redux stores, logic, utility functions) for building a
* LICENSE: Apache-2.0
Copyright 2015-present Mattermost, Inc.
Copyright 2015-present Mattermost, Inc.
Apache License
Version 2.0, January 2004
@@ -1435,7 +1401,7 @@ SOFTWARE.
This product contains 'react-native-doc-viewer' by Philipp Hecht.
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
* HOMEPAGE:
* https://github.com/philipphecht/react-native-doc-viewer/blob/master/README.md
@@ -1665,41 +1631,6 @@ SOFTWARE.
---
## react-native-keyboard-tracking-view
This product contains a modified version of 'react-native-keyboard-tracking-view' by Artal Druk.
React Native UI component which tracks the keyboard
* HOMEPAGE:
* https://github.com/wix/react-native-keyboard-tracking-view
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 Wix.com
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-keychain
This product contains 'react-native-keychain' by Joel Arvidsson.
@@ -1791,7 +1722,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
## react-native-navigation
This product contains a modified version of 'react-native-navigation' by Wix.com.
This product contains a modified version of 'react-native-navigation' by Daniel Zlotin.
React Native Navigation - truly native navigation for iOS and Android
@@ -1862,7 +1793,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product contains 'react-native-passcode-status' by Mark Vayngrib.
check if device-level passcode is supported/enabled/disabled
check passcode status on device
* HOMEPAGE:
* https://github.com/tradle/react-native-passcode-status
@@ -1928,6 +1859,41 @@ SOFTWARE.
---
## react-native-recyclerview-list
This product contains 'react-native-recyclerview-list' by GitHub user "godness84".
A RecyclerView implementation for React Native
* HOMEPAGE:
* https://github.com/godness84/react-native-recyclerview-list#readme
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 Marc Shilling
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' by Masayuki Iwai.
@@ -1976,16 +1942,16 @@ This package simplifies constructing the getItemLayout prop for react native Sec
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.
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
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.
---
@@ -2192,7 +2158,7 @@ SOFTWARE.
## react-native-webview
This product contains a modified version of 'react-native-webview' by Jamon Holmgren.
This product contains 'react-native-webview' by Jamon Holmgren.
React Native WebView component for iOS, Android, and Windows 10 (coming soon)
@@ -2591,7 +2557,7 @@ Display some placeholder stuff before rendering your text or media content in Re
* LICENSE: MIT
Copyright (c) 2004-Today Marvin Frachet
Copyright (c) 2004-2018 Marvin Frachet
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -20,7 +20,7 @@ To help with testing app updates before they're released, you can:
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
- Device information

View File

@@ -75,7 +75,7 @@ import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "metro.config.js"
bundleConfig: "packager-config.js"
]
apply from: "../../node_modules/react-native/react.gradle"
@@ -87,7 +87,7 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
flavorAware: false
]
//apply from: "../../node_modules/react-native-sentry/sentry.gradle"
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
/**
@@ -107,26 +107,22 @@ def enableProguardInReleaseBuilds = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildToolsVersion rootProject.ext.buildToolsVersion
packagingOptions {
pickFirst '**/libjsc.so'
pickFirst '**/libc++_shared.so'
pickFirst 'lib/x86_64/libjsc.so'
pickFirst 'lib/arm64-v8a/libjsc.so'
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 208
versionName "1.21.0"
versionCode 187
versionName "1.18.1"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
abiFilters "armeabi-v7a", "x86"
}
}
@@ -145,7 +141,7 @@ android {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
include "armeabi-v7a", "x86"
}
}
buildTypes {
@@ -169,7 +165,7 @@ android {
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
def versionCodes = ["armeabi-v7a":1, "x86":2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@@ -177,11 +173,6 @@ android {
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
repositories {
@@ -193,6 +184,9 @@ repositories {
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
}
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
@@ -210,9 +204,6 @@ configurations.all {
}
dependencies {
// Make sure to put android-jsc at the top
implementation "org.webkit:android-jsc-intl:r241213"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:28.0.0'
@@ -238,10 +229,9 @@ dependencies {
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':rn-fetch-blob')
implementation project(':react-native-recyclerview-list')
implementation project(':react-native-webview')
implementation project(':react-native-gesture-handler')
implementation project(':@react-native-community_async-storage')
implementation project(':@react-native-community_netinfo')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:1.10.0'

View File

@@ -26,7 +26,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -57,7 +57,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -88,7 +88,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -101,4 +101,4 @@
}
],
"configuration_version": "1"
}
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

View File

@@ -2,6 +2,7 @@
package="com.mattermost.rnbeta">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
@@ -11,15 +12,12 @@
<application
android:name=".MainApplication"
android:allowBackup="false"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"
android:installLocation="auto"
android:networkSecurityConfig="@xml/network_security_config"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
@@ -44,8 +42,7 @@
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:resizeableActivity="true"/>
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"

View File

@@ -4,8 +4,6 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.app.Person.Builder;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.Context;
@@ -96,13 +94,7 @@ public class CustomPushNotification extends PushNotification {
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String type = data.getString("type");
final String ackId = data.getString("ack_id");
int notificationId = MESSAGE_NOTIFICATION_ID;
if (ackId != null) {
notificationReceiptDelivery(ackId, type);
}
if (channelId != null) {
notificationId = channelId.hashCode();
Object objCount = channelIdToNotificationCount.get(channelId);
@@ -121,10 +113,9 @@ public class CustomPushNotification extends PushNotification {
}
synchronized (list) {
if (!"clear".equals(type)) {
String senderName = getSenderName(data.getString("sender_name"), data.getString("channel_name"), data.getString("message"));
String senderName = getSenderName(data.getString("channel_name"), data.getString("message"));
data.putLong("time", new Date().getTime());
data.putString("sender_name", senderName);
data.putString("sender_id", data.getString("sender_id"));
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
@@ -187,21 +178,11 @@ public class CustomPushNotification extends PushNotification {
}
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
String channelId = bundle.getString("channel_id");
String channelName = bundle.getString("channel_name");
String senderName = bundle.getString("sender_name");
String senderId = bundle.getString("sender_id");
String postId = bundle.getString("post_id");
String badge = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
String title = null;
if (version != null && version.equals("v2")) {
title = channelName;
title = bundle.getString("channel_name");
} else {
title = bundle.getString("title");
}
@@ -211,6 +192,13 @@ public class CustomPushNotification extends PushNotification {
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
String badge = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
Bundle b = bundle.getBundle("userInfo");
if (b == null) {
b = new Bundle();
@@ -246,29 +234,12 @@ public class CustomPushNotification extends PushNotification {
CustomPushNotification.badgeCount = badgeCount;
notification.setNumber(badgeCount);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
}
if (android.text.TextUtils.isEmpty(senderName)) {
senderName = getSenderName(senderName, channelName, bundle.getString("message"));
}
String personId = senderId;
if (!android.text.TextUtils.isEmpty(channelName)) {
personId = channelId;
}
Notification.MessagingStyle messagingStyle;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle = new Notification.MessagingStyle("");
} else {
Person sender = new Person.Builder()
.setKey(senderId)
.setName("")
.build();
messagingStyle = new Notification.MessagingStyle(sender);
// HERE ADD THE DOT INDICATOR STUFF
}
if (title != null && (!title.startsWith("@") || channelName != senderName)) {
Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("You");
if (title != null && !title.startsWith("@")) {
messagingStyle
.setConversationTitle(title);
}
@@ -282,26 +253,12 @@ public class CustomPushNotification extends PushNotification {
list.add(bundle);
}
int listCount = list.size() - 1;
for (int i = listCount; i >= 0; i--) {
Bundle data = list.get(i);
for (Bundle data : list) {
String message = data.getString("message");
String previousPersonName = getSenderName(data.getString("sender_name"), channelName, message);
String previousPersonId = data.getString("sender_id");
if (title == null || !android.text.TextUtils.isEmpty(previousPersonName)) {
message = removeSenderFromMessage(previousPersonName, channelName, message);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle.addMessage(message, data.getLong("time"), previousPersonName);
} else {
Person sender = new Person.Builder()
.setKey(previousPersonId)
.setName(previousPersonName)
.build();
messagingStyle.addMessage(message, data.getLong("time"), sender);
if (title == null || !title.startsWith("@")) {
message = removeSenderFromMessage(message);
}
messagingStyle.addMessage(message, data.getLong("time"), data.getString("sender_name"));
}
notification
@@ -401,27 +358,21 @@ public class CustomPushNotification extends PushNotification {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
}
private String getSenderName(String senderName, String channelName, String message) {
if (senderName != null) {
return senderName;
} else if (channelName != null && channelName.startsWith("@")) {
private String getSenderName(String channelName, String message) {
if (channelName != null && channelName.startsWith("@")) {
return channelName;
}
String name = message.split(":")[0];
if (name != message) {
return name;
String senderName = message.split(":")[0];
if (senderName != message) {
return senderName;
}
return " ";
}
private String removeSenderFromMessage(String senderName, String channelName, String message) {
String sender = String.format("%s", getSenderName(senderName, channelName, message));
return message.replaceFirst(sender, "").replaceFirst(": ", "").trim();
}
private void notificationReceiptDelivery(String ackId, String type) {
ReceiptDelivery.send(context, ackId, type);
private String removeSenderFromMessage(String message) {
String sender = String.format("%s: ", getSenderName("", message));
return message.replaceFirst(sender, "");
}
}

View File

@@ -57,6 +57,7 @@ public class InitializationModule extends ReactContextBaseJavaModule {
*
* Miscellaneous:
* MattermostManaged.Config
* replyFromPushNotification
*/
MainApplication app = (MainApplication) mApplication;
@@ -137,6 +138,8 @@ public class InitializationModule extends ReactContextBaseJavaModule {
}
constants.put("managedConfig", config[0]);
constants.put("replyFromPushNotification", app.replyFromPushNotification);
app.replyFromPushNotification = false;
return constants;
}

View File

@@ -22,8 +22,7 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
import com.reactnativecommunity.netinfo.NetInfoPackage;
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
@@ -46,28 +45,12 @@ import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import android.support.annotation.Nullable;
import android.util.Log;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
public Boolean sharedExtensionIsOpened = false;
public long APP_START_TIME;
public long RELOAD;
public long CONTENT_APPEARED;
public long PROCESS_PACKAGES_START;
public long PROCESS_PACKAGES_END;
public Boolean replyFromPushNotification = false;
@Override
public boolean isDebug() {
@@ -101,8 +84,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this),
new AsyncStoragePackage(),
new NetInfoPackage(),
new RNRecyclerviewListPackage(),
new RNCWebViewPackage(),
new RNGestureHandlerPackage()
);
@@ -129,9 +111,6 @@ public class MainApplication extends NavigationApplication implements INotificat
setActivityCallbacks(notificationsLifecycleFacade);
SoLoader.init(this, /* native exopackage */ false);
// Uncomment to listen to react markers for build that has telemetry enabled
// addReactMarkerListener();
}
@Override
@@ -156,36 +135,4 @@ public class MainApplication extends NavigationApplication implements INotificat
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
private void addReactMarkerListener() {
ReactMarker.addListener(new ReactMarker.MarkerListener() {
@Override
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
APP_START_TIME = System.currentTimeMillis();
RELOAD = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
PROCESS_PACKAGES_START = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
PROCESS_PACKAGES_END = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
CONTENT_APPEARED = System.currentTimeMillis();
ReactContext ctx = getReactGateway().getReactContext();
if (ctx != null) {
WritableMap map = Arguments.createMap();
map.putDouble("appReload", RELOAD);
map.putDouble("appContentAppeared", CONTENT_APPEARED);
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("nativeMetrics", map);
}
}
}
});
}
}

View File

@@ -1,9 +1,7 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.bridge.Arguments;
@@ -13,7 +11,6 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
private static MattermostManagedModule instance;
@@ -59,37 +56,10 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
Object result = Arguments.fromBundle(config);
promise.resolve(result);
} else {
promise.resolve(Arguments.createMap());
throw new Exception("The MDM vendor has not sent any Managed configuration");
}
} catch (Exception e) {
promise.resolve(Arguments.createMap());
promise.reject("no managed configuration", e);
}
}
@ReactMethod
// Close the current activity and open the security settings.
public void goToSecuritySettings() {
getReactApplicationContext().startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
getCurrentActivity().finish();
System.exit(0);
}
@ReactMethod
public void isRunningInSplitView(final Promise promise) {
WritableMap result = Arguments.createMap();
Activity current = getCurrentActivity();
if (current != null) {
result.putBoolean("isSplitView", current.isInMultiWindowMode());
} else {
result.putBoolean("isSplitView", false);
}
promise.resolve(result);
}
@ReactMethod
public void quitApp() {
getCurrentActivity().finish();
System.exit(0);
}
}

View File

@@ -21,8 +21,7 @@ public class MattermostPackage implements ReactPackage {
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
MattermostManagedModule.getInstance(reactContext),
NotificationPreferencesModule.getInstance(mApplication, reactContext),
new RNTextInputResetModule(reactContext)
NotificationPreferencesModule.getInstance(mApplication, reactContext)
);
}

View File

@@ -36,17 +36,13 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
final CharSequence message = getReplyMessage(intent);
if (message == null) {
return;
}
mContext = context;
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
final CharSequence message = getReplyMessage(intent);
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
@@ -79,11 +75,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
if (android.text.TextUtils.isEmpty(rootId)) {
rootId = postId;
}
final String rootId = bundle.getString("post_id");
if (token == null || serverUrl == null) {
onReplyFailed(notificationManager, notificationId, channelId);

View File

@@ -1,42 +0,0 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
public class RNTextInputResetModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
public RNTextInputResetModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "RNTextInputReset";
}
// https://github.com/facebook/react-native/pull/12462#issuecomment-298812731
@ReactMethod
public void resetKeyboardInput(final int reactTagToReset) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
imm.restartInput(viewToReset);
}
}
});
}
}

View File

@@ -1,89 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import java.lang.System;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.json.JSONException;
import com.mattermost.react_native_interface.ResolvePromise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.oblador.keychain.KeychainModule;
public class ReceiptDelivery {
public static void send (Context context, final String ackId, final String type) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String[] credentials = map.getString("password").split(",[ ]*");
String token = null;
String serverUrl = null;
if (credentials.length == 2) {
token = credentials[0];
serverUrl = credentials[1];
}
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token));
execute(serverUrl, token, ackId, type);
}
}
});
}
protected static void execute(String serverUrl, String token, String ackId, String type) {
if (token == null || serverUrl == null) {
return;
}
JSONObject json;
long receivedAt = System.currentTimeMillis();
try {
json = new JSONObject();
json.put("id", ackId);
json.put("received_at", receivedAt);
json.put("platform", "android");
json.put("type", type);
} catch (JSONException e) {
Log.e("ReactNative", "Receipt delivery failed to build json payload");
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, json.toString());
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")))
.post(body)
.build();
try {
client.newCall(request).execute();
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
}
}
}

View File

@@ -75,10 +75,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
@ReactMethod
public void close(ReadableMap data) {
this.clear();
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
currentActivity.finish();
}
getCurrentActivity().finish();
if (data != null && data.hasKey("url")) {
ReadableArray files = data.getArray("files");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<base-config>
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />

View File

@@ -5,7 +5,7 @@ buildscript {
buildToolsVersion = "28.0.3"
minSdkVersion = 24
compileSdkVersion = 28
targetSdkVersion = 28
targetSdkVersion = 26
supportLibVersion = "28.0.0"
}
repositories {

View File

@@ -41,9 +41,7 @@ include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-recyclerview-list'
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':@react-native-community_async-storage'
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
include ':@react-native-community_netinfo'
project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')

View File

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import {networkStatusChangedAction} from 'redux-offline';
import {Client4} from 'mattermost-redux/client';
import {DeviceTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch) => {
Client4.setOnline(isOnline);
dispatch(networkStatusChangedAction(isOnline));
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,

View File

@@ -13,36 +13,21 @@ import {
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
selectChannel,
} from 'mattermost-redux/actions/channels';
import {
getPosts,
getPostsBefore,
getPostsSince,
getPostThread,
} from 'mattermost-redux/actions/posts';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {
getChannel,
getCurrentChannelId,
getMyChannelMember,
getRedirectChannelNameForTeam,
getChannelsNameMapInTeam,
} from 'mattermost-redux/selectors/entities/channels';
import {getChannel, getCurrentChannelId, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import telemetry from 'app/telemetry';
import {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannel,
isGroupChannel,
getChannelByName as getChannelByNameSelector,
} from 'mattermost-redux/utils/channel_utils';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
@@ -174,8 +159,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
export function loadPostsIfNecessaryWithRetry(channelId) {
return async (dispatch, getState) => {
const state = getState();
const {posts} = state.entities.posts;
const postsIds = getPostIdsInChannel(state, channelId);
const {posts, postsInChannel} = state.entities.posts;
const postsIds = postsInChannel[channelId];
const actions = [];
const time = Date.now();
@@ -186,7 +171,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
if (received?.order) {
if (received) {
const count = received.order.length;
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
actions.push({
@@ -214,7 +199,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
if (received?.order) {
if (received) {
const count = received.order.length;
loadMorePostsVisible = postsIds.length + count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
actions.push({
@@ -265,14 +250,14 @@ export function loadFilesForPostIfNecessary(postId) {
};
}
export function loadThreadIfNecessary(rootId) {
return (dispatch, getState) => {
export function loadThreadIfNecessary(rootId, channelId) {
return async (dispatch, getState) => {
const state = getState();
const {posts, postsInThread} = state.entities.posts;
const threadPosts = postsInThread[rootId];
const {posts, postsInChannel} = state.entities.posts;
const channelPosts = postsInChannel[channelId];
if (!posts[rootId] || !threadPosts) {
dispatch(getPostThread(rootId));
if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) {
getPostThread(rootId, false)(dispatch, getState);
}
};
}
@@ -339,19 +324,17 @@ export function selectPenultimateChannel(teamId) {
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const state = getState();
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
const channel = getChannelByNameSelector(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
const {channels} = getState().entities.channels;
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
channelId = channel.id;
} else {
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const channels = Object.values(channelsInTeam);
const firstChannel = channels.length ? channels[0].id : {id: ''};
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
channelId = firstChannel.id;
}
@@ -394,7 +377,6 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel,
member,
},
]));
@@ -412,8 +394,7 @@ export function handleSelectChannelByName(channelName, teamName) {
return async (dispatch, getState) => {
const state = getState();
const {teams: currentTeams, currentTeamId} = state.entities.teams;
const currentTeam = currentTeams[currentTeamId];
const currentTeamName = currentTeam?.name;
const currentTeamName = currentTeams[currentTeamId].name;
const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
const currentChannelId = getCurrentChannelId(state);
if (channel && currentChannelId !== channel.id) {
@@ -543,12 +524,6 @@ export function leaveChannel(channel, reset = false) {
}
export function setChannelLoading(loading = true) {
if (loading) {
telemetry.start(['channel:loading']);
} else {
telemetry.end(['channel:loading']);
}
return {
type: ViewTypes.SET_CHANNEL_LOADER,
loading,
@@ -577,7 +552,7 @@ export function setChannelDisplayName(displayName) {
}
// Returns true if there are more posts to load
export function increasePostVisibility(channelId, postId) {
export function increasePostVisibility(channelId, focusedPostId) {
return async (dispatch, getState) => {
const state = getState();
const {loadingPosts, postVisibility} = state.views.channel;
@@ -587,28 +562,22 @@ export function increasePostVisibility(channelId, postId) {
return true;
}
if (!postId) {
// No posts are visible, so the channel is empty
return true;
}
// Check if we already have the posts that we want to show
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (!focusedPostId) {
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
return true;
return true;
}
}
telemetry.reset();
telemetry.start(['posts:loading']);
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
@@ -616,8 +585,14 @@ export function increasePostVisibility(channelId, postId) {
});
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
const page = Math.floor(currentPostVisibility / pageSize);
const result = await retryGetPostsAction(getPostsBefore(channelId, postId, 0, pageSize), dispatch, getState);
let result;
if (focusedPostId) {
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
} else {
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
}
const actions = [{
type: ViewTypes.LOADING_POSTS,
@@ -626,7 +601,7 @@ export function increasePostVisibility(channelId, postId) {
}];
let hasMorePost = false;
if (result?.order) {
if (result) {
const count = result.order.length;
hasMorePost = count >= pageSize;
@@ -646,9 +621,6 @@ export function increasePostVisibility(channelId, postId) {
}
dispatch(batchActions(actions));
telemetry.end(['posts:loading']);
telemetry.save();
return hasMorePost;
};
}

View File

@@ -1,96 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {handleSelectChannelByName} from 'app/actions/views/channel';
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
getChannel: () => ({data: 'received-channel-id'}),
getCurrentChannelId: () => 'current-channel-id',
getMyChannelMember: () => ({data: {member: {}}}),
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Channel', () => {
let store;
const MOCK_SELECT_CHANNEL_TYPE = 'MOCK_SELECT_CHANNEL_TYPE';
const MOCK_RECEIVE_CHANNEL_TYPE = 'MOCK_RECEIVE_CHANNEL_TYPE';
const actions = require('mattermost-redux/actions/channels');
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
if (teamName) {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: 'received-channel-id',
};
}
return {
type: 'MOCK_ERROR',
error: 'error',
};
});
actions.selectChannel = jest.fn().mockReturnValue({
type: MOCK_SELECT_CHANNEL_TYPE,
data: 'selected-channel-id',
});
const currentUserId = 'current-user-id';
const currentChannelId = 'channel-id';
const currentChannelName = 'channel-name';
const currentTeamId = 'current-team-id';
const currentTeamName = 'current-team-name';
const storeObj = {
entities: {
users: {
currentUserId,
},
channels: {
currentChannelId,
},
teams: {
teams: {
currentTeamId,
currentTeams: {
[currentTeamId]: {
name: currentTeamName,
},
},
},
},
},
};
test('handleSelectChannelByName success', async () => {
store = mockStore(storeObj);
await store.dispatch(handleSelectChannelByName(currentChannelName, currentTeamName));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
const selectedChannel = storeBatchActions[0].payload.some((action) => action.type === MOCK_SELECT_CHANNEL_TYPE);
expect(selectedChannel).toBe(true);
});
test('handleSelectChannelByName failure from null currentTeamName', async () => {
const failStoreObj = {...storeObj};
failStoreObj.entities.teams.teams.currentTeamId = 'not-in-current-teams';
store = mockStore(storeObj);
await store.dispatch(handleSelectChannelByName(currentChannelName, null));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(false);
const storeBatchActions = storeActions.some(({type}) => type === 'BATCHING_REDUCER.BATCH');
expect(storeBatchActions).toBe(false);
});
});

View File

@@ -30,7 +30,7 @@ export function executeCommand(message, channelId, rootId) {
const {data, error} = await dispatch(executeCommandService(msg, args));
if (data?.trigger_id) { //eslint-disable-line camelcase
if (data.trigger_id) {
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}

View File

@@ -6,17 +6,6 @@ import {createChannel} from 'mattermost-redux/actions/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {cleanUpUrlable} from 'mattermost-redux/utils/channel_utils';
import {generateId} from 'mattermost-redux/utils/helpers';
export function generateChannelNameFromDisplayName(displayName) {
let name = cleanUpUrlable(displayName);
if (name === '') {
name = generateId();
}
return name;
}
export function handleCreateChannel(displayName, purpose, header, type) {
return async (dispatch, getState) => {
@@ -25,7 +14,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
const teamId = getCurrentTeamId(state);
const channel = {
team_id: teamId,
name: generateChannelNameFromDisplayName(displayName),
name: cleanUpUrlable(displayName),
display_name: displayName,
purpose,
header,

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {generateChannelNameFromDisplayName} from 'app/actions/views/create_channel';
describe('Actions.Views.CreateChannel', () => {
describe('generateChannelNameFromDisplayName', () => {
test('should not change name', async () => {
expect(generateChannelNameFromDisplayName('abc')).toEqual('abc');
});
test('should generate name from non-latin characters', async () => {
expect(generateChannelNameFromDisplayName('熊本').length).toEqual(36);
});
test('should generate name from blank string', async () => {
expect(generateChannelNameFromDisplayName('').length).toEqual(36);
});
});
});

View File

@@ -7,13 +7,12 @@ import {getSessions} from 'mattermost-redux/actions/users';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {app} from 'app/mattermost';
import PushNotifications from 'app/push_notifications';
import {getDeviceTimezone} from 'app/utils/timezone';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
import {setCSRFFromCookie} from 'app/utils/security';
export function handleLoginIdChanged(loginId) {

View File

@@ -2,7 +2,8 @@
// See LICENSE.txt for license information.
import {Posts} from 'mattermost-redux/constants';
import {doPostAction, receivedNewPost} from 'mattermost-redux/actions/posts';
import {PostTypes} from 'mattermost-redux/action_types';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {ViewTypes} from 'app/constants';
@@ -27,7 +28,16 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
},
};
dispatch(receivedNewPost(post));
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post,
},
},
channelId,
});
};
}

View File

@@ -1,12 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {GeneralTypes} from 'mattermost-redux/action_types';
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {receivedNewPost} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {ViewTypes} from 'app/constants';
@@ -111,7 +110,16 @@ export function createPostForNotificationReply(post) {
try {
const data = await Client4.createPost({...newPost, create_at: 0});
dispatch(receivedNewPost(data));
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[data.id]: data,
},
},
channelId: data.channel_id,
});
return {data};
} catch (error) {

View File

@@ -2,8 +2,7 @@
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {Linking, NativeModules, Platform, Text} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
@@ -31,7 +30,6 @@ export default class App {
// Usage: app.js
this.shouldRelaunchWhenActive = false;
this.inBackgroundSince = null;
this.previousAppState = null;
// Usage: screen/entry.js
this.startAppFromPushNotification = false;
@@ -54,6 +52,14 @@ export default class App {
this.token = null;
this.url = null;
// Load polyfill for iOS 9
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS < 10) {
require('@babel/polyfill');
}
}
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
@@ -198,11 +204,10 @@ export default class App {
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
this.token = token;
this.url = url;
if (this.waitForRehydration) {
this.waitForRehydration = false;
this.token = token;
this.url = url;
}
// Only save to keychain if the url and token are set

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as React from 'react';
module.exports = {...React, memo: (x) => x};

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AttachmentButton should match snapshot 1`] = `
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 34,
"justifyContent": "center",
"width": 45,
}
}
>
<Icon
allowFontScaling={false}
color="rgba(61,60,64,0.9)"
name="md-add"
size={30}
style={
Object {
"marginTop": 2,
}
}
/>
</TouchableOpacity>
`;

View File

@@ -1,39 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Fade should render {opacity: 0} 1`] = `
<AnimatedComponent
style={
Object {
"opacity": 0,
"transform": Array [
Object {
"scale": 0,
},
],
}
}
>
<Text>
text
</Text>
</AnimatedComponent>
`;
exports[`Fade should render {opacity: 1} 1`] = `
<AnimatedComponent
style={
Object {
"opacity": 1,
"transform": Array [
Object {
"scale": 1,
},
],
}
}
>
<Text>
text
</Text>
</AnimatedComponent>
`;

View File

@@ -53,6 +53,5 @@ exports[`profile_picture_button should match snapshot 1`] = `
}
}
uploadFiles={[MockFunction]}
validMimeTypes={Array []}
/>
`;

View File

@@ -1,103 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
<View
style={
Object {
"justifyContent": "flex-end",
"paddingHorizontal": 5,
"paddingVertical": 3,
}
}
>
<View
style={
Array [
Object {
"alignItems": "center",
"backgroundColor": "#166de0",
"borderRadius": 18,
"height": 28,
"justifyContent": "center",
"width": 28,
},
Object {
"backgroundColor": "rgba(22,109,224,0.3)",
},
]
}
>
<PaperPlane
color="#ffffff"
height={13}
width={15}
/>
</View>
</View>
`;
exports[`SendButton should match snapshot 1`] = `
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"justifyContent": "flex-end",
"paddingHorizontal": 5,
"paddingVertical": 3,
}
}
>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#166de0",
"borderRadius": 18,
"height": 28,
"justifyContent": "center",
"width": 28,
}
}
>
<PaperPlane
color="#ffffff"
height={13}
width={15}
/>
</View>
</TouchableOpacity>
`;
exports[`SendButton should render theme backgroundColor 1`] = `
<TouchableOpacity
activeOpacity={0.2}
onPress={[MockFunction]}
style={
Object {
"justifyContent": "flex-end",
"paddingHorizontal": 5,
"paddingVertical": 3,
}
}
>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#166de0",
"borderRadius": 18,
"height": 28,
"justifyContent": "center",
"width": 28,
}
}
>
<PaperPlane
color="#ffffff"
height={13}
width={15}
/>
</View>
</TouchableOpacity>
`;

View File

@@ -97,9 +97,9 @@ export default class AtMention extends React.PureComponent {
handleLongPress = async () => {
const {formatMessage} = this.context.intl;
const config = mattermostManaged.getCachedConfig();
const config = await mattermostManaged.getLocalConfig();
if (config?.copyAndPasteProtection !== 'true') {
if (config.copyAndPasteProtection !== 'false') {
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
const actionText = formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'});

View File

@@ -17,8 +17,6 @@ import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
@@ -29,7 +27,6 @@ export default class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
@@ -42,7 +39,6 @@ export default class AttachmentButton extends PureComponent {
navigator: PropTypes.object.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
@@ -51,7 +47,6 @@ export default class AttachmentButton extends PureComponent {
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
@@ -326,14 +321,7 @@ export default class AttachmentButton extends PureComponent {
file.fileName = fileInfo.filename;
}
if (!file.type) {
file.type = lookupMimeType(file.fileName);
}
const {validMimeTypes} = this.props;
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
this.props.onShowUnsupportedMimeTypeWarning();
} else if (file.fileSize > this.props.maxFileSize) {
if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);

View File

@@ -1,74 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
import AttachmentButton from './attachment_button';
jest.mock('react-intl');
describe('AttachmentButton', () => {
const baseProps = {
theme: Preferences.THEMES.default,
navigator: {},
blurTextBox: jest.fn(),
maxFileSize: 10,
uploadFiles: jest.fn(),
};
test('should match snapshot', () => {
const wrapper = shallow(
<AttachmentButton {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should not upload file with invalid MIME type', () => {
const props = {
...baseProps,
validMimeTypes: VALID_MIME_TYPES,
onShowUnsupportedMimeTypeWarning: jest.fn(),
};
const wrapper = shallow(
<AttachmentButton {...props}/>
);
const file = {
type: 'image/gif',
fileSize: 10,
fileName: 'test',
};
wrapper.instance().uploadFiles([file]);
expect(props.onShowUnsupportedMimeTypeWarning).toHaveBeenCalled();
expect(props.uploadFiles).not.toHaveBeenCalled();
});
test('should upload file with valid MIME type', () => {
const props = {
...baseProps,
validMimeTypes: VALID_MIME_TYPES,
onShowUnsupportedMimeTypeWarning: jest.fn(),
};
const wrapper = shallow(
<AttachmentButton {...props}/>
);
const file = {
fileSize: 10,
fileName: 'test',
};
VALID_MIME_TYPES.forEach((mimeType) => {
file.type = mimeType;
wrapper.instance().uploadFiles([file]);
expect(props.onShowUnsupportedMimeTypeWarning).not.toHaveBeenCalled();
expect(props.uploadFiles).toHaveBeenCalled();
});
});
});

View File

@@ -41,7 +41,6 @@ export default class AtMention extends PureComponent {
defaultChannel: {},
isSearch: false,
value: '',
inChannel: [],
};
constructor(props) {
@@ -134,18 +133,18 @@ export default class AtMention extends PureComponent {
return [{
completeHandle: 'all',
id: t('suggestion.mention.all'),
defaultMessage: 'Notifies everyone in this channel',
defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team',
values: {
townsquare: this.props.defaultChannel.display_name,
},
}, {
completeHandle: 'channel',
id: t('suggestion.mention.channel'),
defaultMessage: 'Notifies everyone in this channel',
defaultMessage: 'Notifies everyone in the channel',
}, {
completeHandle: 'here',
id: t('suggestion.mention.here'),
defaultMessage: 'Notifies everyone online in this channel',
defaultMessage: 'Notifies everyone in the channel and online',
}];
};

View File

@@ -24,11 +24,6 @@ export default class AtMentionItem extends PureComponent {
theme: PropTypes.object.isRequired,
};
static defaultProps = {
firstName: '',
lastName: '',
};
completeMention = () => {
const {onPress, username} = this.props;
onPress(username);

View File

@@ -9,8 +9,6 @@ import {
View,
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {DeviceTypes} from 'app/constants';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -22,7 +20,7 @@ import DateSuggestion from './date_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
cursorPosition: PropTypes.number,
cursorPosition: PropTypes.number.isRequired,
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
maxHeight: PropTypes.number,
@@ -31,8 +29,6 @@ export default class Autocomplete extends PureComponent {
theme: PropTypes.object.isRequired,
value: PropTypes.string,
enableDateSuggestion: PropTypes.bool.isRequired,
valueEvent: PropTypes.string,
cursorPositionEvent: PropTypes.string,
};
static defaultProps = {
@@ -41,66 +37,13 @@ export default class Autocomplete extends PureComponent {
enableDateSuggestion: false,
};
static getDerivedStateFromProps(props, state) {
const nextState = {};
let updated = false;
if (props.cursorPosition !== state.cursorPosition && !props.cursorPositionEvent) {
nextState.cursorPosition = props.cursorPosition;
updated = true;
}
if (props.value !== state.value && !props.valueEvent) {
nextState.value = props.value;
updated = true;
}
return updated ? nextState : null;
}
constructor(props) {
super(props);
this.state = {
atMentionCount: 0,
channelMentionCount: 0,
cursorPosition: props.cursorPosition,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
value: props.value,
};
}
componentDidMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
if (this.props.valueEvent) {
EventEmitter.on(this.props.valueEvent, this.handleValueChange);
}
if (this.props.cursorPositionEvent) {
EventEmitter.on(this.props.cursorPositionEvent, this.handleCursorPositionChange);
}
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
if (this.props.valueEvent) {
EventEmitter.off(this.props.valueEvent, this.handleValueChange);
}
if (this.props.cursorPositionEvent) {
EventEmitter.off(this.props.cursorPositionEvent, this.handleCursorPositionChange);
}
}
onChangeText = (value) => {
this.props.onChangeText(value, true);
state = {
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
dateCount: 0,
keyboardOffset: 0,
};
handleAtMentionCountChange = (atMentionCount) => {
@@ -111,10 +54,6 @@ export default class Autocomplete extends PureComponent {
this.setState({channelMentionCount});
};
handleCursorPositionChange = (cursorPosition) => {
this.setState({cursorPosition});
};
handleEmojiCountChange = (emojiCount) => {
this.setState({emojiCount});
};
@@ -127,9 +66,15 @@ export default class Autocomplete extends PureComponent {
this.setState({dateCount});
};
handleValueChange = (value) => {
this.setState({value});
};
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;
@@ -170,7 +115,7 @@ export default class Autocomplete extends PureComponent {
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
@@ -185,43 +130,29 @@ export default class Autocomplete extends PureComponent {
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleAtMentionCountChange}
value={value || ''}
{...this.props}
/>
<ChannelMention
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleChannelMentionCountChange}
value={value || ''}
{...this.props}
/>
<EmojiSuggestion
{...this.props}
cursorPosition={cursorPosition}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleEmojiCountChange}
value={value || ''}
{...this.props}
/>
<SlashSuggestion
{...this.props}
maxListHeight={maxListHeight}
onChangeText={this.onChangeText}
onResultCountChange={this.handleCommandCountChange}
value={value || ''}
{...this.props}
/>
{(this.props.isSearch && this.props.enableDateSuggestion) &&
<DateSuggestion
{...this.props}
cursorPosition={cursorPosition}
onChangeText={this.onChangeText}
onResultCountChange={this.handleIsDateFilterChange}
value={value || ''}
{...this.props}
/>
}
</View>

View File

@@ -44,11 +44,6 @@ export default class ChannelMention extends PureComponent {
static defaultProps = {
isSearch: false,
value: '',
publicChannels: [],
privateChannels: [],
directAndGroupMessages: [],
myChannels: [],
otherChannels: [],
};
constructor(props) {

View File

@@ -17,6 +17,4 @@ function mapStateToProps(state) {
};
}
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);
export default connect(mapStateToProps, null, null, {withRef: true})(Autocomplete);

View File

@@ -29,10 +29,6 @@ class ChannelIntro extends PureComponent {
theme: PropTypes.object.isRequired,
};
static defaultProps = {
currentChannelMembers: [],
};
goToUserProfile = (userId) => {
const {intl, navigator, theme} = this.props;
const options = {
@@ -166,21 +162,29 @@ class ChannelIntro extends PureComponent {
if (creator) {
const creatorName = this.getDisplayName(creator);
mainMessageIntl = {
id: t('intro_messages.creator'),
defaultMessage: 'This is the start of the {name} channel, created by {creator} on {date}.',
id: 'intro_messages.creator',
defaultMessage: 'This is the start of the {name} {type}, created by {creator} on {date}.',
values: {
name: currentChannel.display_name,
creator: creatorName,
date,
type: intl.formatMessage({
id: 'intro_messages.channel',
defaultMessage: 'channel',
}),
},
};
} else {
mainMessageIntl = {
id: t('intro_messages.noCreator'),
defaultMessage: 'This is the start of the {name} channel, created on {date}.',
defaultMessage: 'This is the start of the {name} {type}, created on {date}.',
values: {
name: currentChannel.display_name,
date,
type: intl.formatMessage({
id: 'intro_messages.channel',
defaultMessage: 'channel',
}),
},
};
}
@@ -224,12 +228,16 @@ class ChannelIntro extends PureComponent {
});
const mainMessage = intl.formatMessage({
id: 'intro_messages.creatorPrivate',
defaultMessage: 'This is the start of the {name} private channel, created by {creator} on {date}.',
id: 'intro_messages.creator',
defaultMessage: 'This is the start of the {name} {type}, created by {creator} on {date}.',
}, {
name: currentChannel.display_name,
creator: creatorName,
date,
type: intl.formatMessage({
id: 'intro_messages.group',
defaultMessage: 'private channel',
}),
});
const onlyInvitedMessage = intl.formatMessage({

View File

@@ -1,383 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChannelLoader should match snapshot 1`] = `
<View
onLayout={[Function]}
style={
Array [
Object {
"flex": 1,
},
undefined,
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
<View
style={
Array [
Object {
"backgroundColor": "#ffffff",
"flex": 1,
"flexDirection": "row",
"marginVertical": 10,
"paddingLeft": 12,
"paddingRight": 20,
},
Object {
"backgroundColor": "#ffffff",
},
]
}
>
<ImageContent
animate="fade"
color="rgba(61,60,64,0.2)"
firstLineWidth="80%"
hasRadius={true}
lineNumber={3}
lineSpacing={5}
size={32}
textSize={14}
/>
</View>
</View>
`;

View File

@@ -4,20 +4,16 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
View,
Dimensions,
} from 'react-native';
import {ImageContent} from 'rn-placeholder';
import Placeholder from 'rn-placeholder';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
function calculateMaxRows(height) {
return Math.round(height / 100);
}
export default class ChannelLoader extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@@ -26,35 +22,28 @@ export default class ChannelLoader extends PureComponent {
}).isRequired,
backgroundColor: PropTypes.string,
channelIsLoading: PropTypes.bool.isRequired,
maxRows: PropTypes.number,
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
height: PropTypes.number,
};
constructor(props) {
super(props);
static defaultProps = {
maxRows: 6,
};
const height = props.height || Dimensions.get('window').height;
const maxRows = calculateMaxRows(height);
this.state = {
switch: false,
maxRows,
};
}
state = {
switch: false,
};
static getDerivedStateFromProps(nextProps, prevState) {
const state = {};
if (nextProps.height) {
state.maxRows = calculateMaxRows(nextProps.height);
}
if (!nextProps.channelIsLoading && prevState.switch) {
state.switch = false;
state.channel = null;
return {
switch: false,
channel: null,
};
}
return Object.keys(state) ? state : null;
return null;
}
componentDidMount() {
@@ -87,7 +76,7 @@ export default class ChannelLoader extends PureComponent {
key={key}
style={[style.section, {backgroundColor: bg}]}
>
<ImageContent
<Placeholder.ImageContent
size={32}
animate='fade'
lineNumber={3}
@@ -109,18 +98,8 @@ export default class ChannelLoader extends PureComponent {
}
};
handleLayout = (e) => {
const {height} = e.nativeEvent.layout;
const maxRows = calculateMaxRows(height);
this.setState({maxRows});
}
render() {
const {
channelIsLoading,
style: styleProp,
theme,
} = this.props;
const {channelIsLoading, maxRows, style: styleProp, theme} = this.props;
if (!channelIsLoading) {
return null;
@@ -130,11 +109,8 @@ export default class ChannelLoader extends PureComponent {
const bg = this.props.backgroundColor || theme.centerChannelBg;
return (
<View
style={[style.container, styleProp, {backgroundColor: bg}]}
onLayout={this.handleLayout}
>
{Array(this.state.maxRows).fill().map((item, index) => this.buildSections({
<View style={[style.container, styleProp, {backgroundColor: bg}]}>
{Array(maxRows).fill().map((item, index) => this.buildSections({
key: index,
style,
bg,
@@ -149,6 +125,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
...Platform.select({
android: {
top: 0,
},
ios: {
paddingTop: 15,
},
}),
},
section: {
backgroundColor: theme.centerChannelBg,

View File

@@ -1,29 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import ChannelLoader from './channel_loader';
jest.mock('rn-placeholder', () => ({
ImageContent: () => {},
}));
describe('ChannelLoader', () => {
const baseProps = {
channelIsLoading: true,
theme: Preferences.THEMES.default,
actions: {
handleSelectChannel: jest.fn(),
setChannelLoading: jest.fn(),
},
};
test('should match snapshot', () => {
const wrapper = shallow(<ChannelLoader {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
});

View File

@@ -61,10 +61,6 @@ export default class LastUsers extends React.PureComponent {
usernames: PropTypes.array.isRequired,
};
static defaultProps = {
usernames: [],
};
constructor(props) {
super(props);

View File

@@ -1,22 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {connect} from 'react-redux';
import {makeGenerateCombinedPost} from 'mattermost-redux/utils/post_list';
import Post from 'app/components/post';
export function makeMapStateToProps() {
const generateCombinedPost = makeGenerateCombinedPost();
return (state, ownProps) => {
return {
post: generateCombinedPost(state, ownProps.combinedId),
postId: ownProps.combinedId,
};
};
}
// Note that this also passes through Post's mapStateToProps
export default connect(makeMapStateToProps)(Post);

View File

@@ -24,7 +24,7 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
horizontal={false}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
numColumns={1}
@@ -62,7 +62,7 @@ exports[`CustomList should match snapshot with SectionList 1`] = `
horizontal={false}
initialNumToRender={15}
keyExtractor={[Function]}
keyboardDismissMode="on-drag"
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={16}
onEndReachedThreshold={2}

View File

@@ -3,7 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Keyboard, Platform, SectionList, Text, View} from 'react-native';
import {FlatList, Platform, SectionList, Text, View} from 'react-native';
import {ListTypes} from 'app/constants';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
@@ -26,7 +26,10 @@ export default class CustomList extends PureComponent {
onLoadMore: PropTypes.func,
onRowPress: PropTypes.func,
onRowSelect: PropTypes.func,
renderItem: PropTypes.func.isRequired,
renderItem: PropTypes.oneOfType([
PropTypes.func,
PropTypes.element,
]).isRequired,
selectable: PropTypes.bool,
theme: PropTypes.object.isRequired,
shouldRenderSeparator: PropTypes.bool,
@@ -42,14 +45,6 @@ export default class CustomList extends PureComponent {
super(props);
this.contentOffsetY = 0;
this.keyboardDismissProp = Platform.select({
android: {
onScrollBeginDrag: Keyboard.dismiss,
},
ios: {
keyboardDismissMode: 'on-drag',
},
});
this.state = {};
}
@@ -103,6 +98,12 @@ export default class CustomList extends PureComponent {
props.onPress = onRowPress;
}
// Allow passing in a component like UserListRow or ChannelListRow
if (this.props.renderItem.prototype.isReactComponent) {
const RowComponent = this.props.renderItem;
return <RowComponent {...props}/>;
}
return this.props.renderItem(props);
};
@@ -116,7 +117,7 @@ export default class CustomList extends PureComponent {
data={data}
extraData={extraData}
keyboardShouldPersistTaps='always'
{...this.keyboardDismissProp}
keyboardDismissMode='interactive'
keyExtractor={this.keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ItemSeparatorComponent={this.renderSeparator}
@@ -168,7 +169,7 @@ export default class CustomList extends PureComponent {
contentContainerStyle={style.container}
extraData={loading}
keyboardShouldPersistTaps='always'
{...this.keyboardDismissProp}
keyboardDismissMode='interactive'
keyExtractor={this.keyExtractor}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
ItemSeparatorComponent={this.renderSeparator}

View File

@@ -53,37 +53,40 @@ export default class EditChannelInfo extends PureComponent {
editing: false,
};
constructor(props) {
super(props);
this.nameInput = React.createRef();
this.urlInput = React.createRef();
this.purposeInput = React.createRef();
this.headerInput = React.createRef();
this.lastText = React.createRef();
this.scroll = React.createRef();
}
blur = () => {
if (this.nameInput?.current) {
this.nameInput.current.blur();
if (this.nameInput) {
this.nameInput.blur();
}
// TODO: uncomment below once the channel URL field is added
// if (this.urlInput?.current) {
// this.urlInput.current.blur();
// if (this.urlInput) {
// this.urlInput.blur();
// }
if (this.purposeInput) {
this.purposeInput.blur();
}
if (this.headerInput) {
this.headerInput.blur();
}
if (this.scroll) {
this.scroll.scrollToPosition(0, 0, true);
}
};
if (this.purposeInput?.current) {
this.purposeInput.current.blur();
}
if (this.headerInput?.current) {
this.headerInput.current.blur();
}
channelNameRef = (ref) => {
this.nameInput = ref;
};
if (this.scroll?.current) {
this.scroll.current.scrollToPosition(0, 0, true);
}
channelURLRef = (ref) => {
this.urlInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
@@ -96,6 +99,10 @@ export default class EditChannelInfo extends PureComponent {
}
};
lastTextRef = (ref) => {
this.lastText = ref;
};
canUpdate = (displayName, channelURL, purpose, header) => {
const {
oldDisplayName,
@@ -160,9 +167,13 @@ export default class EditChannelInfo extends PureComponent {
}
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
if (this.scroll?.current && this.lastText?.current) {
this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current));
if (this.scroll && this.lastText) {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
}
};
@@ -212,7 +223,7 @@ export default class EditChannelInfo extends PureComponent {
<View style={style.container}>
<StatusBar/>
<KeyboardAwareScrollView
ref={this.scroll}
ref={this.scrollRef}
style={style.container}
>
{displayError}
@@ -229,7 +240,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.nameInput}
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
@@ -258,7 +269,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.urlInput}
ref={this.channelURLRef}
value={channelURL}
onChangeText={this.onDisplayURLChangeText}
style={style.input}
@@ -288,7 +299,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.purposeInput}
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
@@ -326,7 +337,7 @@ export default class EditChannelInfo extends PureComponent {
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.headerInput}
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
@@ -342,7 +353,7 @@ export default class EditChannelInfo extends PureComponent {
disableFullscreenUI={true}
/>
</View>
<View ref={this.lastText}>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'

View File

@@ -65,7 +65,7 @@ export default class Emoji extends React.PureComponent {
componentWillReceiveProps(nextProps) {
const {displayTextOnly, emojiName, imageUrl} = nextProps;
if (emojiName !== this.props.emojiName && this.mounted) {
if (emojiName !== this.props.emojiName) {
this.setState({
imageUrl: null,
});
@@ -82,11 +82,9 @@ export default class Emoji extends React.PureComponent {
}
setImageUrl = (imageUrl) => {
if (this.mounted) {
this.setState({
imageUrl,
});
}
this.setState({
imageUrl,
});
};
render() {

View File

@@ -33,8 +33,7 @@ function mapStateToProps(state, ownProps) {
config.EnableCustomEmoji !== 'true' ||
config.ExperimentalEnablePostMetadata === 'true' ||
getCurrentUserId(state) === '' ||
!isMinimumServerVersion(Client4.getServerVersion(), 4, 7) ||
isMinimumServerVersion(Client4.getServerVersion(), 5, 12);
!isMinimumServerVersion(Client4.getServerVersion(), 4, 7);
}
return {

View File

@@ -1,52 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Animated} from 'react-native';
export const FADE_DURATION = 100;
export default class Fade extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
style: PropTypes.object,
visible: PropTypes.bool.isRequired,
};
constructor(props) {
super(props);
this.state = {
fadeAnim: new Animated.Value(props.visible ? 1 : 0),
};
}
componentDidUpdate(prevProps) {
if (prevProps.visible !== this.props.visible) {
Animated.timing(
this.state.fadeAnim,
{
toValue: prevProps.visible ? 0 : 1,
duration: FADE_DURATION,
useNativeDriver: true,
}
).start();
}
}
render() {
const {fadeAnim} = this.state;
return (
<Animated.View
style={{
...this.props.style,
opacity: fadeAnim,
transform: [{scale: fadeAnim}],
}}
>
{this.props.children}
</Animated.View>
);
}
}

View File

@@ -1,60 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Animated, Text} from 'react-native';
import {shallow} from 'enzyme';
import Fade from './fade';
jest.useFakeTimers();
describe('Fade', () => {
const baseProps = {
visible: true,
disabled: true,
};
function getWrapper(props = {}) {
const dummyText = 'text';
return shallow(
<Fade
{...baseProps}
{...props}
>
<Text>{dummyText}</Text>
</Fade>
);
}
test('should render {opacity: 1}', () => {
const wrapper = getWrapper({visible: true});
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toHaveStyle('opacity', new Animated.Value(1));
});
test('should render {opacity: 0}', () => {
const wrapper = getWrapper({visible: false});
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toHaveStyle('opacity', new Animated.Value(0));
});
test('should not change opacity when disabled flag is switched', () => {
const wrapper = getWrapper({disabled: false});
expect(wrapper).toHaveStyle('opacity', new Animated.Value(1));
wrapper.setProps({disabled: true});
expect(wrapper).toHaveStyle('opacity', new Animated.Value(1));
});
test('should not change opacity when props stay the same', () => {
const wrapper = getWrapper({visible: true});
expect(wrapper).toHaveStyle('opacity', new Animated.Value(1));
wrapper.setProps({visible: true});
expect(wrapper).toHaveStyle('opacity', new Animated.Value(1));
});
});

View File

@@ -1,9 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PostAttachmentOpenGraph should match snapshot with a single image file 1`] = `
<ScrollViewMock
<ScrollView
horizontal={true}
keyboardShouldPersistTaps="always"
scrollEnabled={false}
style={
Array [
@@ -67,5 +66,5 @@ exports[`PostAttachmentOpenGraph should match snapshot with a single image file
}
}
/>
</ScrollViewMock>
</ScrollView>
`;

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