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
701 changed files with 15469 additions and 24229 deletions

View File

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

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@ build-ios
*.zip
server.PID
mattermost.keystore
tmp/
# OSX
#

View File

@@ -1,185 +1,12 @@
# Mattermost Mobile Apps Changelog
## 1.21.0 Release
- Release Date: July 16, 2019
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- 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 few mobile app crash / fatal error issues.
- Fixed an issue where having the sidebar open at all times on tablets did not work for split view.
- Fixed an issue where new messages were often hidden behind a keyboard or text field.
- Fixed an issue on Android where channel sorting didn't match the web app.
- Fixed an issue where sharing a GIF via keyboard resulted in an error screen.
- Fixed an issue where long-press menu could not be dragged up when rotating the device to landscape view while the menu was open.
- Fixed an issue on Android where push notification settings were only saved after closing the settings page.
- Fixed an issue where users on View Members list had an icon that appeared to be selectable but was not.
- Fixed an issue where "Jump To" showed archived channels the user did not belong to instead of the ones the user was a member of.
- Fixed an issue where changing the timezone setting manually to "Set automatically" did not work on the mobile app.
- Fixed an issue where setting a position field for AD/LDAP sync or SAML in the System Console did not block the user from changing it in account settings.
- Fixed an issue where **Channel Info > Manage/View Members** screen didn't load channel users.
- Fixed an issue where enabling large fonts on iOS caused the left-hand side text to be cut off.
- Fixed an issue on Android where users could not reply to a push notification if the mention was in a thread message.
### Known Issues
- (Android) On subpath server, logging in using GitLab or OneLogin fails to display Mattermost. [MM-16829](https://mattermost.atlassian.net/browse/MM-16829)
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
- Android apps slow down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
## 1.20.2 Release
- Release Date: July 10, 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 Moto G7 devices were detected as tablets and showed a fixed width sidebar.
- Fixed an issue where having the sidebar open at all times on tablets did not work on split view.
## 1.20.1 Release
- Release Date: June 21, 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 some Android devices were crashing.
- Fixed an issue where messages were missing after reconnecting the network.
## 1.20.0 Release
- Release Date: June 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.
### Highlights
#### Tablet Improvements
- Channel sidebar now remains open at a fixed width on tablet devices.
#### iOS Keyboard Dismissal
- If the keyboard is open, swiping down past it now closes it.
#### Profile Telemetry for Android Beta Builds
- To improve Android app performance, we are collecting trace events and device information, collectively known as metrics, to identify slow performing key areas. Those metrics will be sent only from users using Android app beta build starting in version v1.20, who are logged in to servers that allow sending [diagnostic information](https://docs.mattermost.com/administration/config-settings.html#enable-diagnostics-and-error-reporting).
### Improvements
- Increased the double tap delay for post action buttons.
- Implemented assets for Adaptive icons.
- Users are now brought to the bottom of the channel when posting a message.
- Users can now execute actions while the keyboard is open.
- Added support on iOS for IPv6 on LTE networks.
- Added support for LDAP Group constrained feature with v5.12 servers.
### Bug Fixes
- Fixed an issue where a post wasn't immediately removed when deleting another user's post.
- Fixed an issue where the cursor jumped back when typing after auto-completing a slash command.
- Fixed an issue where the iOS app didnt properly restore its connection after disconnect.
- Fixed an issue where the long press menu persisted after returning from a thread.
- Fixed an issue on Android where the "Write to [channel name]" was cut off for group messages with several users.
- Fixed an issue where users were not able to flag or unflag posts in a read-only channel.
- Fixed an issue where the progress indicator was negative while downloading a video.
- Fixed an issue where the edit post modal didnt have an autocorrect.
- Fixed an issue where the 'I forgot my password' option was available on the mobile client even with Email Authentication disabled on the server.
- Fixed an issue with large separation between placeholders on iPad when a channel was loading.
- Fixed an issue where "Show More" was not removed after the post was edited to a single line.
### Known Issues
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
## 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

@@ -1,6 +1,5 @@
.PHONY: pre-run pre-build clean
.PHONY: check-style
.PHONY: i18n-extract-ci
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
@@ -77,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
@@ -90,6 +90,8 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e "s|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java
@sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@@ -159,7 +161,7 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
@@ -174,20 +176,20 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
fi; \
fi
build: | stop pre-build check-style i18n-extract-ci ## Builds the app for Android & iOS
build: | stop pre-build check-style ## Builds the app for Android & iOS
$(call start_packager)
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
$(call stop_packager)
build-ios: | stop pre-build check-style i18n-extract-ci ## Builds the iOS app
build-ios: | stop pre-build check-style ## Builds the iOS app
$(call start_packager)
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
$(call stop_packager)
build-android: | stop pre-build check-style i18n-extract-ci prepare-android-build ## Build the Android app
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
$(call start_packager)
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
@@ -201,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)
@@ -214,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
@@ -222,13 +222,12 @@ 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
@npm test
build-pr: | can-build-pr stop pre-build check-style i18n-extract-ci ## Build a PR from the mattermost-mobile repo
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
$(call start_packager)
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
@@ -241,16 +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
i18n-extract-ci:
mkdir -p tmp
cp assets/base/i18n/en.json tmp/en.json
mkdir -p tmp/fake-webapp-dir/i18n/
echo '{}' > tmp/fake-webapp-dir/i18n/en.json
npm run mmjstool -- i18n extract-mobile --webapp-dir tmp/fake-webapp-dir --mobile-dir .
diff tmp/en.json assets/base/i18n/en.json
rm -rf tmp
## 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.
@@ -312,37 +278,6 @@ SOFTWARE.
---
## core-js
This product contains 'core-js' by Denis Pushkarev.
Modular standard library for JavaScript.
* HOMEPAGE:
* https://github.com/zloirock/core-js
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## deep-equal
This product contains 'deep-equal' by James Halliday.
@@ -375,39 +310,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## deepmerge
This product contains 'deepmerge' by Josh Duff.
A library for deep (recursive) merging of Javascript objects.
* HOMEPAGE:
* https://github.com/TehShrike/deepmerge
* LICENSE: The MIT License (MIT)
Copyright (c) 2012 James Halliday, Josh Duff, 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.
---
## emoji-regex
This product contains 'emoji-regex' by Mathias Bynens.
@@ -823,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
@@ -1280,6 +1182,41 @@ SOFTWARE.
---
## react-native-bottom-sheet
This product contains 'react-native-bottom-sheet' by WhatAKitty.
React Native Bottom sheet for android
* HOMEPAGE:
* https://github.com/WhatAKitty/react-native-bottom-sheet#readme
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 WhatAKitty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-button
This product contains 'react-native-button' by James Ide.
@@ -1464,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
@@ -1694,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.
@@ -1820,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
@@ -1891,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
@@ -1957,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.
@@ -2005,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.
---
@@ -2221,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)
@@ -2620,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
@@ -32,7 +32,7 @@ To help with testing app updates before they're released, you can:
### Contribute Code
1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) for issues marked as [Help Wanted]
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted]
2. Comment to let people know youre working on it
3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions

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"
@@ -107,27 +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
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 221
versionName "1.22.0"
versionCode 187
versionName "1.18.1"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
abiFilters "armeabi-v7a", "x86"
}
}
@@ -146,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 {
@@ -170,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 =
@@ -178,11 +173,6 @@ android {
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
repositories {
@@ -194,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'
}
@@ -211,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'
@@ -226,6 +216,7 @@ dependencies {
implementation project(':react-native-video')
implementation project(':react-native-navigation')
implementation project(':react-native-image-picker')
implementation project(':react-native-bottom-sheet')
implementation project(':react-native-device-info')
implementation project(':reactnativenotifications')
implementation project(':react-native-cookies')
@@ -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

@@ -0,0 +1,146 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.support.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
import com.mattermost.react_native_interface.ResolvePromise;
import com.oblador.keychain.KeychainModule;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class InitializationModule extends ReactContextBaseJavaModule {
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
static final String APP_BACKGROUND = "APP_BACKGROUND";
private final Application mApplication;
public InitializationModule(Application application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
@Override
public String getName() {
return "Initialization";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
/**
* Package all native module variables in constants
* in order to avoid the native bridge
*
* KeyStore:
* credentialsExist
* deviceToken
* currentUserId
* token
* url
*
* AsyncStorage:
* toolbarBackground
* toolbarTextColor
* appBackground
*
* Miscellaneous:
* MattermostManaged.Config
* replyFromPushNotification
*/
MainApplication app = (MainApplication) mApplication;
final Boolean[] credentialsExist = {false};
final WritableMap[] credentials = {null};
final Object[] config = {null};
// Get KeyStore credentials
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
module.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
credentialsExist[0] = false;
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
credentialsExist[0] = true;
credentials[0] = map;
}
}
});
// Get managedConfig from MattermostManagedModule
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
WritableNativeMap nativeMap = (WritableNativeMap) value;
config[0] = value;
}
});
// Get AsyncStorage key/values
final ArrayList<String> keys = new ArrayList<String>(5);
keys.add(TOOLBAR_BACKGROUND);
keys.add(TOOLBAR_TEXT_COLOR);
keys.add(APP_BACKGROUND);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
if (toolbarBackground != null
&& toolbarTextColor != null
&& appBackground != null) {
constants.put("themesExist", true);
constants.put("toolbarBackground", toolbarBackground);
constants.put("toolbarTextColor", toolbarTextColor);
constants.put("appBackground", appBackground);
} else {
constants.put("themesExist", false);
}
if (credentialsExist[0]) {
constants.put("credentialsExist", true);
constants.put("credentials", credentials[0]);
} else {
constants.put("credentialsExist", false);
}
constants.put("managedConfig", config[0]);
constants.put("replyFromPushNotification", app.replyFromPushNotification);
app.replyFromPushNotification = false;
return constants;
}
}

View File

@@ -0,0 +1,36 @@
package com.mattermost.rnbeta;
import android.app.Application;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class InitializationPackage implements ReactPackage {
private final Application mApplication;
public InitializationPackage(Application application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -2,14 +2,12 @@ package com.mattermost.rnbeta;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.reactnativenavigation.controllers.SplashActivity;
import com.reactnativenavigation.NavigationActivity;
public class MainActivity extends NavigationActivity {
public class MainActivity extends SplashActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.launch_screen);
/**
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
@@ -25,4 +23,9 @@ public class MainActivity extends NavigationActivity {
return;
}
}
@Override
public int getSplashLayout() {
return R.layout.launch_screen;
}
}

View File

@@ -1,47 +1,41 @@
package com.mattermost.rnbeta;
import com.mattermost.share.SharePackage;
import com.mattermost.share.RealPathUtil;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.content.Context;
import android.content.RestrictionsManager;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.util.HashMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.mattermost.share.ShareModule;
import com.learnium.RNDeviceInfo.RNDeviceModule;
import com.imagepicker.ImagePickerModule;
import com.psykar.cookiemanager.CookieManagerModule;
import com.oblador.vectoricons.VectorIconsModule;
import com.wix.reactnativenotifications.RNNotificationsModule;
import io.tradle.react.LocalAuthModule;
import com.gantix.JailMonkey.JailMonkeyModule;
import com.RNFetchBlob.RNFetchBlob;
import io.sentry.RNSentryModule;
import io.sentry.RNSentryEventEmitter;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule;
import com.inprogress.reactnativeyoutube.YouTubeStandaloneModule;
import com.reactlibrary.RNReactNativeDocViewerModule;
import com.reactnativedocumentpicker.DocumentPicker;
import com.oblador.keychain.KeychainModule;
import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
import com.reactnativecommunity.netinfo.NetInfoModule;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.oblador.keychain.KeychainPackage;
import com.reactlibrary.RNReactNativeDocViewerPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.horcrux.svg.SvgPackage;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
import com.imagepicker.ImagePickerPackage;
import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.react.NavigationReactNativeHost;
import com.reactnativenavigation.react.ReactGateway;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
@@ -51,48 +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.ReactNativeHost;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.soloader.SoLoader;
import com.mattermost.share.RealPathUtil;
import android.util.Log;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public static MainApplication instance;
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;
private Bundle mManagedConfig = null;
@Override
protected ReactGateway createReactGateway() {
ReactNativeHost host = new NavigationReactNativeHost(this, isDebug(), createAdditionalReactPackages()) {
@Override
protected String getJSMainModuleName() {
return "index";
}
};
return new ReactGateway(this, isDebug(), host);
}
public Boolean replyFromPushNotification = false;
@Override
public boolean isDebug() {
@@ -105,114 +63,61 @@ public class MainApplication extends NavigationApplication implements INotificat
// Add the packages you require here.
// No need to add RnnPackage and MainReactPackage
return Arrays.<ReactPackage>asList(
new TurboReactPackage() {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
switch (name) {
case "MattermostShare":
return new ShareModule(instance, reactContext);
case "RNDeviceInfo":
return new RNDeviceModule(reactContext, false);
case "ImagePickerManager":
return new ImagePickerModule(reactContext, R.style.DefaultExplainingPermissionsTheme);
case "RNCookieManagerAndroid":
return new CookieManagerModule(reactContext);
case "RNVectorIconsModule":
return new VectorIconsModule(reactContext);
case "WixRNNotifications":
return new RNNotificationsModule(instance, reactContext);
case "RNLocalAuth":
return new LocalAuthModule(reactContext);
case "JailMonkey":
return new JailMonkeyModule(reactContext);
case "RNFetchBlob":
return new RNFetchBlob(reactContext);
case "MattermostManaged":
return MattermostManagedModule.getInstance(reactContext);
case "NotificationPreferences":
return NotificationPreferencesModule.getInstance(instance, reactContext);
case "RNTextInputReset":
return new RNTextInputResetModule(reactContext);
case "RNSentry":
return new RNSentryModule(reactContext);
case "RNSentryEventEmitter":
return new RNSentryEventEmitter(reactContext);
case "ReactNativeExceptionHandler":
return new ReactNativeExceptionHandlerModule(reactContext);
case "YouTubeStandaloneModule":
return new YouTubeStandaloneModule(reactContext);
case "RNReactNativeDocViewer":
return new RNReactNativeDocViewerModule(reactContext);
case "RNDocumentPicker":
return new DocumentPicker(reactContext);
case "RNKeychainManager":
return new KeychainModule(reactContext);
case AsyncStorageModule.NAME:
return new AsyncStorageModule(reactContext);
case NetInfoModule.NAME:
return new NetInfoModule(reactContext);
default:
throw new IllegalArgumentException("Could not find module " + name);
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("RNDeviceInfo", new ReactModuleInfo("RNDeviceInfo", "com.learnium.RNDeviceInfo.RNDeviceModule", false, false, true, false, false));
map.put("ImagePickerManager", new ReactModuleInfo("ImagePickerManager", "com.imagepicker.ImagePickerModule", false, false, false, false, false));
map.put("RNCookieManagerAndroid", new ReactModuleInfo("RNCookieManagerAndroid", "com.psykar.cookiemanager.CookieManagerModule", false, false, false, false, false));
map.put("RNVectorIconsModule", new ReactModuleInfo("RNVectorIconsModule", "com.oblador.vectoricons.VectorIconsModule", false, false, false, false, false));
map.put("WixRNNotifications", new ReactModuleInfo("WixRNNotifications", "com.wix.reactnativenotifications.RNNotificationsModule", false, false, false, false, false));
map.put("RNLocalAuth", new ReactModuleInfo("RNLocalAuth", "io.tradle.react.LocalAuthModule", false, false, false, false, false));
map.put("JailMonkey", new ReactModuleInfo("JailMonkey", "com.gantix.JailMonkey.JailMonkeyModule", false, false, true, false, false));
map.put("RNFetchBlob", new ReactModuleInfo("RNFetchBlob", "com.RNFetchBlob.RNFetchBlob", false, false, true, false, false));
map.put("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
map.put("RNSentryEventEmitter", new ReactModuleInfo("RNSentryEventEmitter", "com.sentry.RNSentryEventEmitter", false, false, true, false, false));
map.put("ReactNativeExceptionHandler", new ReactModuleInfo("ReactNativeExceptionHandler", "com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule", false, false, false, false, false));
map.put("YouTubeStandaloneModule", new ReactModuleInfo("YouTubeStandaloneModule", "com.inprogress.reactnativeyoutube.YouTubeStandaloneModule", false, false, false, false, false));
map.put("RNReactNativeDocViewer", new ReactModuleInfo("RNReactNativeDocViewer", "com.reactlibrary.RNReactNativeDocViewerModule", false, false, false, false, false));
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "com.reactnativedocumentpicker.DocumentPicker", false, false, false, false, false));
map.put("RNKeychainManager", new ReactModuleInfo("RNKeychainManager", "com.oblador.keychain.KeychainModule", false, false, true, false, false));
map.put(AsyncStorageModule.NAME, new ReactModuleInfo(AsyncStorageModule.NAME, "com.reactnativecommunity.asyncstorage.AsyncStorageModule", false, false, false, false, false));
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
return map;
}
};
}
},
new RNCWebViewPackage(),
new ImagePickerPackage(),
new RNBottomSheetPackage(),
new RNDeviceInfo(),
new CookieManagerPackage(),
new VectorIconsPackage(),
new SvgPackage(),
new LinearGradientPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
new RNFetchBlobPackage(),
new MattermostPackage(this),
new RNSentryPackage(),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
new RNReactNativeDocViewerPackage(),
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this),
new RNRecyclerviewListPackage(),
new RNCWebViewPackage(),
new RNGestureHandlerPackage()
);
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
registerActivityLifecycleCallbacks(new ManagedActivityLifecycleCallbacks());
// Delete any previous temp files created by the app
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
RealPathUtil.deleteTempFiles(tempFolder);
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
SoLoader.init(this, /* native exopackage */ false);
// Create an object of the custom facade impl
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
// Attach it to react-native-navigation
setActivityCallbacks(notificationsLifecycleFacade);
// Uncomment to listen to react markers for build that has telemetry enabled
// addReactMarkerListener();
SoLoader.init(this, /* native exopackage */ false);
}
@Override
public boolean clearHostOnActivityDestroy(Activity activity) {
// This solves the issue where the splash screen does not go away
// after the app is killed by the OS cause of memory or a long time in the background
return false;
}
@Override
@@ -220,7 +125,7 @@ public class MainApplication extends NavigationApplication implements INotificat
return new CustomPushNotification(
context,
bundle,
defaultFacade,
notificationsLifecycleFacade, // Instead of defaultFacade!!!
defaultAppLaunchHelper,
new JsIOHelper()
);
@@ -230,81 +135,4 @@ public class MainApplication extends NavigationApplication implements INotificat
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
public ReactContext getRunningReactContext() {
final ReactGateway reactGateway = getReactGateway();
if (reactGateway == null) {
return null;
}
return reactGateway
.getReactNativeHost()
.getReactInstanceManager()
.getCurrentReactContext();
}
public synchronized Bundle loadManagedConfig(Context ctx) {
if (ctx != null) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE);
mManagedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
return mManagedConfig;
}
return null;
}
return null;
}
public synchronized Bundle getManagedConfig() {
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
return mManagedConfig;
}
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
return loadManagedConfig(ctx);
}
return null;
}
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 = getRunningReactContext();
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,145 +0,0 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.util.ArraySet;
import android.util.Log;
import java.util.Set;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class ManagedActivityLifecycleCallbacks implements ActivityLifecycleCallbacks {
private static final String TAG = ManagedActivityLifecycleCallbacks.class.getSimpleName();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context ctx, Intent intent) {
if (ctx != null) {
Bundle managedConfig = MainApplication.instance.loadManagedConfig(ctx);
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i(TAG, "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (managedConfig != null && activity != null) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@Override
public void onActivityResumed(Activity activity) {
ReactContext ctx = MainApplication.instance.getRunningReactContext();
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (ctx != null) {
Bundle newConfig = MainApplication.instance.loadManagedConfig(ctx);
if (!equalBundles(newConfig, managedConfig)) {
Log.i(TAG, "onResumed Managed Configuration Changed");
sendConfigChanged(newConfig);
}
}
}
@Override
public void onActivityStopped(Activity activity) {
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (managedConfig != null) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
private void sendConfigChanged(Bundle config) {
WritableMap result = Arguments.createMap();
if (config != null) {
result = Arguments.fromBundle(config);
}
ReactContext ctx = MainApplication.instance.getRunningReactContext();
if (ctx != null) {
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("managedConfigDidChange", result);
}
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

View File

@@ -1,41 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.oblador.keychain.KeychainModule;
import com.mattermost.react_native_interface.ResolvePromise;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
public class MattermostCredentialsHelper {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
final KeychainModule keychainModule = new KeychainModule(context);
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
final ArrayList<String> keys = new ArrayList<String>(1);
keys.add(CURRENT_SERVER_URL);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
}
}

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;
@@ -53,43 +50,16 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getConfig(final Promise promise) {
try {
Bundle config = MainApplication.instance.getManagedConfig();
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
if (config != null) {
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

@@ -0,0 +1,32 @@
package com.mattermost.rnbeta;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class MattermostPackage implements ReactPackage {
private final MainApplication mApplication;
public MattermostPackage(MainApplication application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
MattermostManagedModule.getInstance(reactContext),
NotificationPreferencesModule.getInstance(mApplication, reactContext)
);
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList();
}
}

View File

@@ -24,6 +24,7 @@ import okhttp3.Response;
import com.mattermost.react_native_interface.ResolvePromise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.oblador.keychain.KeychainModule;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
@@ -35,30 +36,34 @@ 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);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
String channelId = bundle.getString("channel_id");
onReplyFailed(notificationManager, notificationId, channelId);
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
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("URL=%s TOKEN=%s", serverUrl, token));
replyToMessage(serverUrl, token, notificationId, message);
@@ -70,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

@@ -0,0 +1,251 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.content.res.Configuration;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.controllers.ActivityCallbacks;
import com.reactnativenavigation.react.ReactGateway;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade {
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private static NotificationsLifecycleFacade instance;
private Bundle managedConfig = null;
private Activity mVisibleActivity;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
if (context != null) {
// Get the current configuration bundle
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) context
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i("ReactNative", "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
public static NotificationsLifecycleFacade getInstance() {
if (instance == null) {
instance = new NotificationsLifecycleFacade();
}
return instance;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
if (activity != null) {
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
}
@Override
public void onActivityResumed(Activity activity) {
switchToVisible(activity);
ReactContext ctx = getRunningReactContext();
if (managedConfig != null && managedConfig.size() > 0 && ctx != null) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx
.getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
if (!equalBundles(newConfig ,managedConfig)) {
Log.i("ReactNative", "onResumed Managed Configuration Changed");
managedConfig = newConfig;
sendConfigChanged(managedConfig);
}
}
}
@Override
public void onActivityPaused(Activity activity) {
switchToInvisible(activity);
}
@Override
public void onActivityStopped(Activity activity) {
switchToInvisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
public void onActivityDestroyed(Activity activity) {
switchToInvisible(activity);
}
@Override
public boolean isReactInitialized() {
return NavigationApplication.instance.isReactContextInitialized();
}
@Override
public ReactContext getRunningReactContext() {
final ReactGateway reactGateway = NavigationApplication.instance.getReactGateway();
if (reactGateway == null || !reactGateway.isInitialized()) {
return null;
}
return reactGateway.getReactContext();
}
@Override
public boolean isAppVisible() {
return mVisibleActivity != null;
}
@Override
public synchronized void addVisibilityListener(AppVisibilityListener listener) {
mListeners.add(listener);
}
@Override
public synchronized void removeVisibilityListener(AppVisibilityListener listener) {
mListeners.remove(listener);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (mVisibleActivity != null) {
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
mVisibleActivity.sendBroadcast(intent);
}
}
private synchronized void switchToVisible(Activity activity) {
if (mVisibleActivity == null) {
mVisibleActivity = activity;
Log.v(TAG, "Activity is now visible ("+activity+")");
for (AppVisibilityListener listener : mListeners) {
listener.onAppVisible();
}
}
}
private synchronized void switchToInvisible(Activity activity) {
if (mVisibleActivity == activity) {
mVisibleActivity = null;
Log.v(TAG, "Activity is now NOT visible ("+activity+")");
for (AppVisibilityListener listener : mListeners) {
listener.onAppNotVisible();
}
}
}
public synchronized void LoadManagedConfig(ReactContext ctx) {
if (ctx != null) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
}
public synchronized Bundle getManagedConfig() {
if (managedConfig!= null && managedConfig.size() > 0) {
return managedConfig;
}
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
LoadManagedConfig(ctx);
return managedConfig;
}
return null;
}
public void sendConfigChanged(Bundle config) {
Object result = Arguments.fromBundle(config);
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

View File

@@ -1,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,84 +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.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.react_native_interface.ResolvePromise;
public class ReceiptDelivery {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
public static void send (Context context, final String ackId, final String type) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
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,14 +5,12 @@ buildscript {
buildToolsVersion = "28.0.3"
minSdkVersion = 24
compileSdkVersion = 28
targetSdkVersion = 28
targetSdkVersion = 26
supportLibVersion = "28.0.0"
}
repositories {
jcenter()
google()
mavenLocal()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
@@ -39,7 +37,6 @@ subprojects {
allprojects {
repositories {
google()
mavenCentral()
mavenLocal()
jcenter()
maven {
@@ -50,8 +47,5 @@ allprojects {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
}
maven {
url "https://jitpack.io"
}
}
}

View File

@@ -22,9 +22,11 @@ project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_m
include ':react-native-local-auth'
project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android')
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/lib/android/app/')
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-bottom-sheet'
project(':react-native-bottom-sheet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bottom-sheet/android')
include ':react-native-device-info'
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
include ':react-native-cookies'
@@ -39,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

@@ -1,394 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import EphemeralStore from 'app/store/ephemeral_store';
export function resetToChannel(passProps = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'Channel',
passProps,
options: {
layout: {
backgroundColor: 'transparent',
},
statusBar: {
visible: true,
},
topBar: {
visible: false,
height: 0,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
},
},
}],
},
},
});
};
}
export function resetToSelectServer(allowOtherServers) {
return (dispatch, getState) => {
const theme = getTheme(getState());
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name: 'SelectServer',
passProps: {
allowOtherServers,
},
options: {
statusBar: {
visible: true,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
visible: false,
height: 0,
},
},
},
}],
},
},
});
};
}
export function resetToTeams(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
visible: true,
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
},
};
Navigation.setRoot({
root: {
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
},
});
};
}
export function goToScreen(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const state = getState();
const componentId = EphemeralStore.getNavigationTopComponentId();
const theme = getTheme(state);
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function popTopScreen(screenId) {
return () => {
if (screenId) {
Navigation.pop(screenId);
} else {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.pop(componentId);
}
};
}
export function popToRoot() {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.popToRoot(componentId).catch(() => {
// RNN returns a promise rejection if there are no screens
// atop the root screen to pop. We'll do nothing in this
// case but we will catch the rejection here so that the
// caller doesn't have to.
});
};
}
export function showModal(name, title, passProps = {}, options = {}) {
return (dispatch, getState) => {
const theme = getTheme(getState());
const defaultOptions = {
layout: {
backgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
},
topBar: {
animate: true,
visible: true,
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
text: title,
},
leftButtonColor: theme.sidebarHeaderTextColor,
rightButtonColor: theme.sidebarHeaderTextColor,
},
};
Navigation.showModal({
stack: {
children: [{
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
}],
},
});
};
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
return (dispatch) => {
const title = '';
const animationsEnabled = (Platform.OS === 'android').toString();
const defaultOptions = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enabled: animationsEnabled,
alpha: {
from: 0,
to: 1,
duration: 250,
},
},
dismissModal: {
enabled: animationsEnabled,
alpha: {
from: 1,
to: 0,
duration: 250,
},
},
},
};
const mergeOptions = merge(defaultOptions, options);
dispatch(showModal(name, title, passProps, mergeOptions));
};
}
export function showSearchModal(initialValue = '') {
return (dispatch) => {
const name = 'Search';
const title = '';
const passProps = {initialValue};
const options = {
topBar: {
visible: false,
height: 0,
},
};
dispatch(showModal(name, title, passProps, options));
};
}
export function dismissModal(options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
Navigation.dismissModal(componentId, options).catch(() => {
// RNN returns a promise rejection if there is no modal to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function dismissAllModals(options = {}) {
return () => {
Navigation.dismissAllModals(options).catch(() => {
// RNN returns a promise rejection if there are no modals to
// dismiss. We'll do nothing in this case but we will catch
// the rejection here so that the caller doesn't have to.
});
};
}
export function peek(name, passProps = {}, options = {}) {
return () => {
const componentId = EphemeralStore.getNavigationTopComponentId();
const defaultOptions = {
preview: {
commit: false,
},
};
Navigation.push(componentId, {
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
return () => {
Navigation.mergeOptions(componentId, {
topBar: {
...buttons,
},
});
};
}
export function showOverlay(name, passProps, options = {}) {
return () => {
const defaultOptions = {
overlay: {
interceptTouchOutside: false,
},
};
Navigation.showOverlay({
component: {
name,
passProps,
options: merge(defaultOptions, options),
},
});
};
}
export function dismissOverlay(componentId) {
return () => {
return Navigation.dismissOverlay(componentId).catch(() => {
// RNN returns a promise rejection if there is no modal with
// this componentId to dismiss. We'll do nothing in this case
// but we will catch the rejection here so that the caller
// doesn't have to.
});
};
}
export function applyTheme(componentId, skipBackButtonStyle = false) {
return (dispatch, getState) => {
const theme = getTheme(getState());
let backButton = {
color: theme.sidebarHeaderTextColor,
};
if (skipBackButtonStyle && Platform.OS === 'android') {
backButton = null;
}
Navigation.mergeOptions(componentId, {
topBar: {
backButton,
background: {
color: theme.sidebarHeaderBg,
},
title: {
color: theme.sidebarHeaderTextColor,
},
},
});
};
}

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, selectTeam} from 'mattermost-redux/actions/teams';
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';
@@ -54,8 +39,8 @@ import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels'
const MAX_POST_TRIES = 3;
export function loadChannelsIfNecessary(teamId) {
return async (dispatch) => {
await dispatch(fetchMyChannelsAndMembers(teamId));
return async (dispatch, getState) => {
await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
};
}
@@ -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({
@@ -198,7 +183,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
});
}
} else {
const lastConnectAt = state.websocket?.lastConnectAt || 0;
const {lastConnectAt} = state.device.websocket;
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
let since;
@@ -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;
}
@@ -378,7 +361,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
const actions = [
dispatch(batchActions([
selectChannel(channelId),
setChannelDisplayName(channel.display_name),
{
@@ -391,30 +374,18 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
teamId: currentTeamId,
channelId,
},
];
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
member,
},
]));
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
actions.push({
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: currentChannelId,
channel: getChannel(state, currentChannelId),
member: getMyChannelMember(state, currentChannelId),
});
}
if (!fromPushNotification) {
actions.push({
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel,
member,
});
}
dispatch(batchActions(actions));
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
@@ -423,16 +394,9 @@ 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 (teamName && teamName !== currentTeamName) {
const team = getTeamByName(state, teamName);
dispatch(selectTeam(team));
}
if (channel && currentChannelId !== channel.id) {
dispatch(handleSelectChannel(channel.id));
}
@@ -560,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,
@@ -594,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;
@@ -604,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,
@@ -633,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,
@@ -643,7 +601,7 @@ export function increasePostVisibility(channelId, postId) {
}];
let hasMorePost = false;
if (result?.order) {
if (result) {
const count = result.order.length;
hasMorePost = count >= pageSize;
@@ -663,9 +621,6 @@ export function increasePostVisibility(channelId, postId) {
}
dispatch(batchActions(actions));
telemetry.end(['posts:loading']);
telemetry.save();
return hasMorePost;
};
}

View File

@@ -1,224 +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 initialState from 'app/initial_state';
import testHelper from 'test/test_helper';
import {
handleSelectChannelByName,
loadPostsIfNecessaryWithRetry,
} from 'app/actions/views/channel';
import postReducer from 'mattermost-redux/reducers/entities/posts';
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 MOCK_RECEIVED_POSTS = 'RECEIVED_POSTS';
const MOCK_RECEIVED_POSTS_IN_CHANNEL = 'RECEIVED_POSTS_IN_CHANNEL';
const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE';
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 postActions = require('mattermost-redux/actions/posts');
postActions.getPostsSince = jest.fn(() => {
return {
type: MOCK_RECEIVED_POSTS_SINCE,
data: {
order: [],
posts: {},
},
};
});
postActions.getPosts = jest.fn((channelId) => {
const order = [];
const posts = {};
for (let i = 0; i < 60; i++) {
const p = testHelper.fakePost(channelId);
order.push(p.id);
posts[p.id] = p;
}
return {
type: MOCK_RECEIVED_POSTS,
data: {
order,
posts,
},
};
});
const postUtils = require('mattermost-redux/utils/post_utils');
postUtils.getLastCreateAt = jest.fn((array) => {
return array[0].create_at;
});
let nextPostState = {};
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 = {
...initialState,
entities: {
...initialState.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);
});
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
store = mockStore(storeObj);
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
expect(postActions.getPosts).toBeCalled();
const storeActions = store.getActions();
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
const receivedPosts = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS);
const receivedPostsAtAction = storeBatchActions[0].payload.some((action) => action.type === 'RECEIVED_POSTS_FOR_CHANNEL_AT_TIME');
nextPostState = postReducer(store.getState().entities.posts, receivedPosts);
nextPostState = postReducer(nextPostState, {
type: MOCK_RECEIVED_POSTS_IN_CHANNEL,
channelId: currentChannelId,
data: receivedPosts.data,
recent: true,
});
expect(receivedPostsAtAction).toBe(true);
});
test('loadPostsIfNecessaryWithRetry get posts since', async () => {
store = mockStore({
...storeObj,
entities: {
...storeObj.entities,
posts: nextPostState,
},
views: {
...storeObj.views,
channel: {
...storeObj.views.channel,
lastGetPosts: {
[currentChannelId]: Date.now(),
},
},
},
});
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
const storeActions = store.getActions();
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
expect(postUtils.getLastCreateAt).toBeCalled();
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, Object.values(store.getState().entities.posts.posts)[0].create_at);
expect(receivedPostsSince).not.toBe(null);
});
test('loadPostsIfNecessaryWithRetry get posts since the websocket reconnected', async () => {
const time = Date.now();
store = mockStore({
...storeObj,
entities: {
...storeObj.entities,
posts: nextPostState,
},
views: {
...storeObj.views,
channel: {
...storeObj.views.channel,
lastGetPosts: {
[currentChannelId]: time,
},
},
},
websocket: {
lastConnectAt: time + (1 * 60 * 1000),
},
});
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
const storeActions = store.getActions();
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
expect(postUtils.getLastCreateAt).not.toBeCalled();
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, store.getState().views.channel.lastGetPosts[currentChannelId]);
expect(receivedPostsSince).not.toBe(null);
});
});

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 {setAppCredentials} from 'app/init/credentials';
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) {
@@ -45,7 +44,7 @@ export function handleSuccessfulLogin() {
const currentUserId = getCurrentUserId(state);
await setCSRFFromCookie(url);
setAppCredentials(deviceToken, currentUserId, token, url);
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
@@ -56,6 +55,7 @@ export function handleSuccessfulLogin() {
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url,
token,
},
});

View File

@@ -11,8 +11,10 @@ import {
handlePasswordChanged,
} from 'app/actions/views/login';
jest.mock('app/init/credentials', () => ({
setAppCredentials: () => jest.fn(),
jest.mock('app/mattermost', () => ({
app: {
setAppCredentials: () => jest.fn(),
},
}));
jest.mock('react-native-cookies', () => ({

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

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {ViewTypes} from 'app/constants';
export function handleSearchDraftChanged(text) {
@@ -11,3 +13,25 @@ export function handleSearchDraftChanged(text) {
}, getState);
};
}
export function showSearchModal(navigator, initialValue = '') {
return (dispatch, getState) => {
const theme = getTheme(getState());
const options = {
screen: 'Search',
animated: true,
backButtonTitle: '',
overrideBackPress: true,
passProps: {
initialValue,
},
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: theme.centerChannelBg,
},
};
navigator.showModal(options);
};
}

View File

@@ -16,10 +16,6 @@ export function handleServerUrlChanged(serverUrl) {
};
}
export function setServerUrl(serverUrl) {
return {type: ViewTypes.SERVER_URL_CHANGED, serverUrl};
}
export default {
handleServerUrlChanged,
};

View File

@@ -5,7 +5,7 @@ import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
export function userTyping(channelId, rootId) {
return async (dispatch, getState) => {
const {websocket} = getState();
const {websocket} = getState().device;
if (websocket.connected) {
wsUserTyping(channelId, rootId)(dispatch, getState);
}

321
app/app.js Normal file
View File

@@ -0,0 +1,321 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {setDeepLinkURL} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTranslations as getLocalTranslations} from 'app/i18n';
import {store, handleManagedConfig} from 'app/mattermost';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import {setCSRFFromCookie} from 'app/utils/security';
const {Initialization} = NativeModules;
const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND';
const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR';
const APP_BACKGROUND = 'APP_BACKGROUND';
export default class App {
constructor() {
// Usage: app.js
this.shouldRelaunchWhenActive = false;
this.inBackgroundSince = null;
// Usage: screen/entry.js
this.startAppFromPushNotification = false;
this.isNotificationsConfigured = false;
this.allowOtherServers = true;
this.appStarted = false;
this.emmEnabled = false;
this.performingEMMAuthentication = false;
this.translations = null;
this.toolbarBackground = null;
this.toolbarTextColor = null;
this.appBackground = null;
// Usage utils/push_notifications.js
this.replyNotificationData = null;
this.deviceToken = null;
// Usage credentials
this.currentUserId = null;
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);
this.setFontFamily();
this.getStartupThemes();
this.getAppCredentials();
}
setFontFamily = () => {
// Set a global font for Android
if (Platform.OS === 'android') {
const defaultFontFamily = {
style: {
fontFamily: 'Roboto',
},
};
const TextRender = Text.render;
const initialDefaultProps = Text.defaultProps;
Text.defaultProps = {
...initialDefaultProps,
...defaultFontFamily,
};
Text.render = function render(props, ...args) {
const oldProps = props;
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
try {
return Reflect.apply(TextRender, this, [newProps, ...args]);
} finally {
newProps = oldProps;
}
};
}
};
getTranslations = () => {
if (this.translations) {
return this.translations;
}
const state = store.getState();
const locale = getCurrentLocale(state);
this.translations = getLocalTranslations(locale);
return this.translations;
};
getAppCredentials = async () => {
try {
const credentials = await avoidNativeBridge(
() => {
return Initialization.credentialsExist;
},
() => {
return Initialization.credentials;
},
() => {
this.waitForRehydration = true;
return getGenericPassword();
}
);
if (credentials) {
const usernameParsed = credentials.username.split(',');
const passwordParsed = credentials.password.split(',');
// username == deviceToken, currentUserId
// password == token, url
if (usernameParsed.length === 2 && passwordParsed.length === 2) {
const [deviceToken, currentUserId] = usernameParsed;
const [token, url] = passwordParsed;
// if for any case the url and the token aren't valid proceed with re-hydration
if (url && url !== 'undefined' && token && token !== 'undefined') {
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
await setCSRFFromCookie(url);
} else {
this.waitForRehydration = true;
}
}
} else {
this.waitForRehydration = false;
}
} catch (error) {
return null;
}
return null;
};
getStartupThemes = async () => {
try {
const [
toolbarBackground,
toolbarTextColor,
appBackground,
] = await avoidNativeBridge(
() => {
return Initialization.themesExist;
},
() => {
return [
Initialization.toolbarBackground,
Initialization.toolbarTextColor,
Initialization.appBackground,
];
},
() => {
return Promise.all([
AsyncStorage.getItem(TOOLBAR_BACKGROUND),
AsyncStorage.getItem(TOOLBAR_TEXT_COLOR),
AsyncStorage.getItem(APP_BACKGROUND),
]);
}
);
if (toolbarBackground) {
this.toolbarBackground = toolbarBackground;
this.toolbarTextColor = toolbarTextColor;
this.appBackground = appBackground;
}
} catch (error) {
return null;
}
return null;
};
setPerformingEMMAuthentication = (authenticating) => {
this.performingEMMAuthentication = authenticating;
};
setAppCredentials = (deviceToken, currentUserId, token, url) => {
if (!currentUserId) {
return;
}
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
if (this.waitForRehydration) {
this.waitForRehydration = false;
this.token = token;
this.url = url;
}
// Only save to keychain if the url and token are set
if (url && token) {
try {
setGenericPassword(username, password);
} catch (e) {
console.warn('could not set credentials', e); //eslint-disable-line no-console
}
}
};
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
AsyncStorage.setItem(TOOLBAR_BACKGROUND, toolbarBackground);
AsyncStorage.setItem(TOOLBAR_TEXT_COLOR, toolbarTextColor);
AsyncStorage.setItem(APP_BACKGROUND, appBackground);
};
setStartAppFromPushNotification = (startAppFromPushNotification) => {
this.startAppFromPushNotification = startAppFromPushNotification;
};
setIsNotificationsConfigured = (isNotificationsConfigured) => {
this.isNotificationsConfigured = isNotificationsConfigured;
};
setAllowOtherServers = (allowOtherServers) => {
this.allowOtherServers = allowOtherServers;
};
setAppStarted = (appStarted) => {
this.appStarted = appStarted;
};
setEMMEnabled = (emmEnabled) => {
this.emmEnabled = emmEnabled;
};
setDeviceToken = (deviceToken) => {
this.deviceToken = deviceToken;
};
setReplyNotificationData = (replyNotificationData) => {
this.replyNotificationData = replyNotificationData;
};
setInBackgroundSince = (inBackgroundSince) => {
this.inBackgroundSince = inBackgroundSince;
};
setShouldRelaunchWhenActive = (shouldRelaunchWhenActive) => {
this.shouldRelaunchWhenActive = shouldRelaunchWhenActive;
};
clearNativeCache = () => {
resetGenericPassword();
AsyncStorage.multiRemove([
TOOLBAR_BACKGROUND,
TOOLBAR_TEXT_COLOR,
APP_BACKGROUND,
]);
};
handleDeepLink = (event) => {
const {url} = event;
store.dispatch(setDeepLinkURL(url));
}
launchApp = async () => {
const shouldStart = await handleManagedConfig();
if (shouldStart) {
this.startApp();
}
};
startApp = () => {
if (this.appStarted || this.waitForRehydration) {
return;
}
const {dispatch} = store;
Linking.getInitialURL().then((url) => {
dispatch(setDeepLinkURL(url));
});
let screen = 'SelectServer';
if (this.token && this.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
try {
dispatch(loadMe());
} catch (e) {
// Fall through since we should have a previous version of the current user because we have a token
console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console
}
}
switch (screen) {
case 'SelectServer':
EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true);
break;
case 'Channel':
EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true);
break;
}
this.setAppStarted(true);
}
}

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

@@ -1,14 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`profile_picture_button should match snapshot 1`] = `
<Connect(AttachmentButton)
<AttachmentButton
blurTextBox={[MockFunction]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={
Array [
null,
]
}
maxFileCount={5}
maxFileSize={20971520}
navigator={
Object {
"dismissModal": [MockFunction],
"push": [MockFunction],
"setButtons": [MockFunction],
"setOnNavigatorEvent": [MockFunction],
}
}
theme={
Object {
"awayIndicator": "#ffbc42",

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

@@ -18,14 +18,12 @@ const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
@@ -54,16 +52,23 @@ export default class AnnouncementBanner extends PureComponent {
}
handlePress = () => {
const {actions} = this.props;
const {intl} = this.context;
const {navigator, theme} = this.props;
const screen = 'ExpandedAnnouncementBanner';
const title = intl.formatMessage({
id: 'mobile.announcement_banner.title',
defaultMessage: 'Announcement',
navigator.push({
screen: 'ExpandedAnnouncementBanner',
title: this.context.intl.formatMessage({
id: 'mobile.announcement_banner.title',
defaultMessage: 'Announcement',
}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
actions.goToScreen(screen, title);
};
toggleBanner = (show = true) => {

View File

@@ -12,14 +12,12 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
goToScreen: jest.fn(),
},
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,
bannerText: 'Banner Text',
bannerTextColor: '#fff',
navigator: {},
theme: Preferences.THEMES.default,
};

View File

@@ -1,14 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import AnnouncementBanner from './announcement_banner';
function mapStateToProps(state) {
@@ -26,12 +23,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
export default connect(mapStateToProps)(AnnouncementBanner);

View File

@@ -3,7 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Text} from 'react-native';
import {Clipboard, Platform, Text} from 'react-native';
import {intlShape} from 'react-intl';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
@@ -14,12 +14,10 @@ import BottomSheet from 'app/utils/bottom_sheet';
export default class AtMention extends React.PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToScreen: PropTypes.func.isRequired,
}).isRequired,
isSearchResult: PropTypes.bool,
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
navigator: PropTypes.object.isRequired,
onPostPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
teammateNameDisplay: PropTypes.string,
@@ -50,15 +48,29 @@ export default class AtMention extends React.PureComponent {
}
goToUserProfile = () => {
const {actions} = this.props;
const {navigator, theme} = this.props;
const {intl} = this.context;
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
userId: this.state.user.id,
const options = {
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
backButtonTitle: '',
passProps: {
userId: this.state.user.id,
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
};
actions.goToScreen(screen, title, passProps);
if (Platform.OS === 'ios') {
navigator.push(options);
} else {
navigator.showModal(options);
}
};
getUserDetailsFromMentionName(props) {
@@ -85,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

@@ -1,15 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {goToScreen} from 'app/actions/navigation';
import AtMention from './at_mention';
function mapStateToProps(state) {
@@ -20,12 +17,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToScreen,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AtMention);
export default connect(mapStateToProps)(AtMention);

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';
@@ -27,12 +25,8 @@ const ShareExtension = NativeModules.MattermostShare;
export default class AttachmentButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
showModalOverCurrentContext: PropTypes.func.isRequired,
}).isRequired,
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
validMimeTypes: PropTypes.array,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
@@ -42,9 +36,9 @@ export default class AttachmentButton extends PureComponent {
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
navigator: PropTypes.object.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
onShowUnsupportedMimeTypeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
@@ -53,7 +47,6 @@ export default class AttachmentButton extends PureComponent {
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
validMimeTypes: [],
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
@@ -328,20 +321,28 @@ 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);
}
};
handleFileAttachmentOption = (action) => {
this.props.navigator.dismissModal({
animationType: 'none',
});
// Have to wait to launch the library attachment action.
// If we call the action after dismissModal with no delay then the
// Wix navigator will dismiss the library attachment modal as well.
setTimeout(() => {
if (typeof action === 'function') {
action();
}
}, 100);
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
@@ -353,7 +354,6 @@ export default class AttachmentButton extends PureComponent {
maxFileCount,
onShowFileMaxWarning,
extraOptions,
actions,
} = this.props;
if (fileCount === maxFileCount) {
@@ -366,7 +366,7 @@ export default class AttachmentButton extends PureComponent {
if (canTakePhoto) {
items.push({
action: this.attachPhotoFromCamera,
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
@@ -377,7 +377,7 @@ export default class AttachmentButton extends PureComponent {
if (canTakeVideo) {
items.push({
action: this.attachVideoFromCamera,
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
@@ -388,7 +388,7 @@ export default class AttachmentButton extends PureComponent {
if (canBrowsePhotoLibrary) {
items.push({
action: this.attachFileFromLibrary,
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
text: {
id: t('mobile.file_upload.library'),
defaultMessage: 'Photo Library',
@@ -399,7 +399,7 @@ export default class AttachmentButton extends PureComponent {
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
action: this.attachVideoFromLibraryAndroid,
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
text: {
id: t('mobile.file_upload.video'),
defaultMessage: 'Video Library',
@@ -410,7 +410,7 @@ export default class AttachmentButton extends PureComponent {
if (canBrowseFiles) {
items.push({
action: this.attachFileFromFiles,
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
@@ -427,7 +427,21 @@ export default class AttachmentButton extends PureComponent {
});
}
actions.showModalOverCurrentContext('OptionsModal', {items});
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
animationType: 'none',
passProps: {
items,
},
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext',
},
});
};
render() {

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,76 +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 = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
theme: Preferences.THEMES.default,
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

@@ -1,19 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {showModalOverCurrentContext} from 'app/actions/navigation';
import AttachmentButton from './attachment_button';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
showModalOverCurrentContext,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(AttachmentButton);

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>

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