Compare commits

..

29 Commits

Author SHA1 Message Date
Elias Nahum
920e46f9a1 Bump app build number to 175 (#2573) 2019-02-15 13:36:57 -08:00
Elias Nahum
fac489a5d0 Fix a crash when displaying the app in russian 2019-02-15 09:50:02 -03:00
Elias Nahum
cbb9a2b749 Bump app build number to 174 (#2572) 2019-02-14 13:56:33 -08:00
JoramWilander
b382f81cf6 Update mattermost-redux 2019-02-14 13:49:38 -05:00
Dean Whillier
66cd2a98ac [MM 13934] Prevent blank mention keys from crashing the app (#2570)
* don’t match blank word boundaries

* test for blank mention keys

* Check for blank mention before looking for match

* account for multiple spaces as a blank mention
2019-02-13 15:07:06 -08:00
Sudheer
317e3b14ab Fix alignment of count on channel drawer (#2569) 2019-02-13 20:58:14 +08:00
Martin Kraft
87af5404ae MM-14055: Load the channel even if not joining. (#2567) 2019-02-13 05:40:38 -05:00
Elias Nahum
4f3679e09d translations PR 20190211 (#2565) 2019-02-12 23:24:30 -03:00
Harrison Healey
976a29ed29 MM-14001 Don't mount PostBodyAdditionalContent for system messages (#2564) 2019-02-12 15:41:53 -05:00
Elias Nahum
ffef7e2bcf MM-13337 Fix EMM connections using VPN on-demand (#2558)
* Fix EMM connections using VPN on-demand

* Update package.json

* Update package-lock.json
2019-02-11 10:43:34 -08:00
Elias Nahum
e5e741840e Bump app build number to 173 (#2557) 2019-02-08 18:11:47 -03:00
Elias Nahum
83ec54b8c8 Add support for relative permalinks (#2555) 2019-02-08 22:56:55 +08:00
Elias Nahum
226bfa8dd2 Fix placeholder image on Android when profile pic is removed (#2551) 2019-02-07 09:21:39 -03:00
Elias Nahum
1ebf6fc0c2 MM-13974 Missing translations (#2549)
* Missing translations

* update snapshots
2019-02-05 19:16:18 -03:00
Elias Nahum
d40b1da1d0 Fix previewing doc files more than once on Android (#2550) 2019-02-05 19:14:03 -03:00
Elias Nahum
b24c97b7e9 translations PR 20190204 (#2547) 2019-02-05 12:17:03 -03:00
Dan Maas
5d61ae3342 Update NOTICE.txt (#2544)
- Minor copyright owner updates
2019-02-04 21:20:26 -03:00
Elias Nahum
ccc33cbf39 Bump app build number to 172 (#2543) 2019-02-02 15:51:02 -03:00
Elias Nahum
43a0d25636 MM-13859 set the auth token when fetching images from the server (#2537)
* MM-13859 set the auth token when fetching images from the server

* set the image_cache siteURL from redux

* HH suggestion

Co-Authored-By: enahum <nahumhbl@gmail.com>

* Removing comment
2019-02-01 19:38:19 -03:00
Elias Nahum
860d4c582f MM-13897 preserve iOS push notifications when opening the app and set the proper badge count (#2541) 2019-02-01 19:37:24 -03:00
Daniel Schalla
c930bf85af [MM-13839] Remove possibility to change eMail within mobile app (#2540)
* Make eMail setting read only

* Add translation entry, change wording

* Update wording
2019-02-01 19:35:55 -03:00
Sudheer
97e2647eae MM-13866 Never ending loader if app is un authenticated (#2539)
* if token or url is not present setState of loaded flag to true
2019-01-31 20:12:55 +05:30
Brad Coughlin
24ff55f4c0 MM-13876 React Native: Webhook icon is misaligned and bottom edges are cut off (#2536) 2019-01-30 13:27:05 -08:00
George Goldberg
95fb8a9348 Bump redux version. (#2531) 2019-01-30 16:40:58 +00:00
Elias Nahum
2ded5182f3 translations PR 20190128 (#2532) 2019-01-30 13:22:20 -03:00
Saturnino Abril
83b0cfb9ba [MM-13412 & MM-13711] Allow showing of all post options if possible to fit 75% of the screen height (#2501)
* allow showing all options if possible to fit 60% of the screen height

* change max initial position from 60% to 70%

* fix broken UI of the bottom part of post options

* change to 75%
2019-01-30 01:09:59 +08:00
Elias Nahum
d29998957b Bump Version to 1.16.0 and Build number 171 (#2530)
* Bump app build number to 171

* Bump app version number to 1.16.0
2019-01-28 13:23:15 -03:00
Sudheer
7e182f6022 MM-13525 Fix alignment of jewel on channel drawer icon (#2473)
* MM-13525 Fix alignement of jewel on channel drawer icon
Fix position of dot
Fix snapshot
Change unread indicator to be View instead of . text

* Fix position of dot for unread indicator in badge

* Update snapshot
2019-01-28 12:52:24 -03:00
Elias Nahum
7fa7cf4364 MM-13846 Alert boxes to follow UX Guidelines (#2525) 2019-01-28 12:49:41 -03:00
622 changed files with 13073 additions and 25333 deletions

View File

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

2
.gitignore vendored
View File

@@ -1,6 +1,5 @@
assets/override
dist
build-ios
*.zip
server.PID
mattermost.keystore
@@ -81,7 +80,6 @@ ios/sentry.properties
# Testing
.nyc_output
coverage
# Pods
.podinstall

View File

@@ -1,145 +1,5 @@
# Mattermost Mobile Apps Changelog
## 1.19.0 Release
- Release Date: May 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where Android managed config was lost on the thread view.
- Fixed an issue where contents of ephemeral posts did not display on the mobile app.
- Fixed a few mobile app crash / fatal error issues.
- Fixed an issue with an expanding animation when tapping on Jump to Channel in the channel list.
- Fixed an issue on iOS where animated custom emoji weren't animated.
- Fixed an issue on iOS where users were unable to create channel name of 2 characters.
- Fixed an issue on iOS where emoji appeared too close, with uneven spacing, and too small in the info modal.
- Added an error handler when sharing text that was over server's maximum post size with the iOS Share Extension.
- Fixed an issue where users could upload a GIF as a profile image.
### Known Issues
- Buttons inside ephemeral posts are not clickable / functional on the mobile app.
## 1.18.1 Release
- Release Date: April 18, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed a crash issue caused by a malformed post textbox localize string.
- Fixed an issue where iOS crashed when trying to log in using SSO and the SSO provider set a cookie without an expiration date.
## 1.18.0 Release
- Release Date: April 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
- ``Bot`` tags were added for bot accounts feature in server v5.10 and mobile v1.18, meaning that mobile v1.17 and earlier don't support the tags.
### Highlights
- Added support for Office365 single sign-on (SSO).
- Added support for Integrated Windows Authentication (IWA).
### Improvements
- Added the ability for channel links to open inside the app.
- Added ability for emojis and hyperlinks to render in the message attachment title.
- Added Chinese support for words that trigger mentions.
- Added a setting to the system console to change the minimum length of hashtags.
- Added a reply option to long press context menu.
### Bug Fixes
- Fixed an issue where blank spaces broke markdown tables.
- Fixed an issue where deactivated users appeared on "Add Members" modal but not on the search results.
- Fixed an issue on Android where extra text in the search box appeared after using the autocomplete drop-down.
- Fixed an issue with multiple text entries when typing with Shift+Letter on Android.
- Fixed an issue where push notifications badges did not always clear when read on another device.
- Fixed an issue where opening a single or group notification did not take the user into the channel where the notification came from.
- Fixed an issue where timezone did not automatically update on Android when travelling to another timezone.
- Fixed an issue where the user mention autocomplete drop-down was case sensitive.
- Fixed an issue where system admininistrators were able to see the full long press menu when long pressing a system message.
- Fixed an issue where users were not able to unflag posts from "Flagged Posts" when opened from a read-only channel.
- Fixed an issue where users were unable to create channel names of 2 byte characters.
### Known Issues
- Content for ephemeral messages is not displayed on Mattermost Mobile Apps.
## 1.17.0 Release
- Release Date: March 20, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
- If you are using an EMM provider via AppConfig, make sure to add two new settings, `useVPN` and `timeoutVPN`, to your AppConfig file. The settings were added for EMM connections using VPN on-demand - one to indicate if every request should wait for the VPN connection to be established, and another to set the timeout in seconds. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- 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
- iOS Share Extension now supports large file sizes and improved performance
### Bug Fixes
- Fixed support for EMM connections using VPN on-demand. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Fixed several Android app crash / fatal error issues.
- Fixed an issue on Android where the app crashed intermittently when selecting a link.
- Fixed an issue where email notifications setting was out of sync with the webapp until the setting was edited.
- Fixed an issue where notification badges were not cleared from other clients when clicking on a push notification after opening the mobile app.
- Fixed an issue where the app did not show local notification when session expired.
- Fixed an issue where the profile picture for webhooks was showing the hook owner picture.
- Fixed an issue where some emoji were not rendered as jumbo.
- Fixed an issue where jumbo emoji posted as a reply sometimes appeared with large space beneath.
- Fixed an issue where the "No Internet Connection" banner did not always display when internet connectivity was lost.
- Fixed an issue where the "No Internet Connection" banner did not always disappear when connection was re-estabilished.
- Fixed an issue where opening channels with unreads had loading indicator placed above unread messages line.
## 1.16.1 Release
- Release Date: February 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).
### Bug Fixes
- Fixed an issue where link previews and reactions weren't displayed when post metadata was disabled.
- Fixed an issue on Android where the app crashed when sharing multiple files.
## 1.16.0 Release
- Release Date: February 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).
### Improvements
- Added the ability to remove own profile picture.
- Changed "X" to "Cancel" on Edit Profile page.
- Added support for relative permalinks.
### Bug Fixes
- Fixed an issue where the iOS app did not wait until the on-demand VPN connection was established. (EMM Providers)
- Fixed an issue with a white screen caused by missing Russian translations.
- Fixed an issue where the iOS badge notification did not always clear.
- Fixed an issue where the thread view displayed a new message indicator.
- Fixed an issue where quick multiple taps on the file icon opened multiple file previews.
- Fixed an issue where the settings page did not show an option to join other teams.
- Fixed an issue where image previews didn't work after using Delete File Cache.
- Fixed an issue on Android where the notification trigger word modal title was "Send email notifications" instead of "Keywords".
- Fixed an issue where the Webhook icon was misaligned and bottom edges were cut off.
- Fixed an issue on Android where the user was not asked to authenticate to the app first when trying to share a photo, resulting in a white "Share modal" screen with a never-ending loading indicator.
- Fixed an issue on iOS where push notifications were not preserved when opening the app via the Mattermost icon.
## 1.15.2 Release
- Release Date: January 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

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.

104
Makefile
View File

@@ -2,7 +2,7 @@
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
.PHONY: build build-ios build-android unsigned-ios unsigned-android
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: test help
@@ -76,12 +76,6 @@ 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 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
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@@ -97,10 +91,21 @@ post-install:
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
start: | pre-run ## Starts the React Native packager server
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start; \
else \
echo React Native packager server already running; \
fi
stop: ## Stops the React Native packager server
$(call stop_packager)
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
check-device-ios:
@if ! [ $(shell which xcodebuild) ]; then \
@@ -176,64 +181,68 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
fi
build: | stop pre-build check-style ## Builds the app for Android & iOS
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
$(call stop_packager)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
build-ios: | stop pre-build check-style ## Builds the iOS app
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
$(call stop_packager)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
$(call stop_packager)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building unsigned iOS app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-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 ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -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)
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
$(call start_packager)
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
@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)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@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)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
test: | pre-run check-style ## Runs tests
@npm test
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
$(call start_packager)
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
$(call stop_packager)
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
can-build-pr:
@if [ -z ${PR_ID} ]; then \
@@ -242,27 +251,10 @@ can-build-pr:
fi
i18n-extract: ## Extract strings for translation from the source code
npm run mmjstool -- i18n extract-mobile
@[[ -d $(MM_UTILITIES_DIR) ]] || echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
@[[ -d $(MM_UTILITIES_DIR) ]] && cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-mobile
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
define start_packager
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
else \
echo React Native packager server already running; \
fi
endef
define stop_packager
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
endef

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.
@@ -344,42 +310,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## emoji-regex
This product contains 'emoji-regex' by Mathias Bynens.
A regular expression to match all Emoji-only symbols as per the Unicode Standard.
* HOMEPAGE:
* https://mths.be/emoji-regex
* LICENSE: MIT
MIT License
Copyright Mathias Bynens <https://mathiasbynens.be/>
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.
---
## fuse.js
This product contains 'fuse.js' by Kirollos Risk.
@@ -759,7 +689,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
@@ -1403,7 +1333,7 @@ This product contains a modified version of 'react-native-device-info' by Rebecc
Get device information using react-native
* HOMEPAGE:
* https://github.com/react-native-community/react-native-device-info#readme
* https://github.com/rebeccahughes/react-native-device-info#readme
* LICENSE: MIT
@@ -1435,7 +1365,7 @@ SOFTWARE.
This product contains 'react-native-doc-viewer' by Philipp Hecht.
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
* HOMEPAGE:
* https://github.com/philipphecht/react-native-doc-viewer/blob/master/README.md
@@ -1665,41 +1595,6 @@ SOFTWARE.
---
## react-native-keyboard-tracking-view
This product contains a modified version of 'react-native-keyboard-tracking-view' by Artal Druk.
React Native UI component which tracks the keyboard
* HOMEPAGE:
* https://github.com/wix/react-native-keyboard-tracking-view
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2016 Wix.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-keychain
This product contains 'react-native-keychain' by Joel Arvidsson.
@@ -1791,7 +1686,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 'react-native-navigation' by Daniel Zlotin.
React Native Navigation - truly native navigation for iOS and Android
@@ -1862,7 +1757,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
@@ -1897,7 +1792,7 @@ SOFTWARE.
This product contains 'react-native-permissions' by Yonah Forst.
Check and request user permissions in React Native.
Check user permissions in React Native
* HOMEPAGE:
* https://github.com/yonahforst/react-native-permissions
@@ -1928,6 +1823,41 @@ SOFTWARE.
---
## react-native-recyclerview-list
This product contains 'react-native-recyclerview-list' by GitHub user "godness84".
A RecyclerView implementation for React Native
* HOMEPAGE:
* https://github.com/godness84/react-native-recyclerview-list#readme
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 Marc Shilling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-safe-area
This product contains 'react-native-safe-area' by Masayuki Iwai.
@@ -1976,16 +1906,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.
---
@@ -2120,6 +2050,43 @@ SOFTWARE.
---
## react-native-tableview
This product contains 'react-native-tableview' by Pavlo Aksonov.
Native iOS TableView wrapper for React Native
* HOMEPAGE:
* https://github.com/aksonov/react-native-tableview#readme
* LICENSE: BSD-2-Clause
Copyright (c) 2015, aksonov
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## react-native-vector-icons
This product contains 'react-native-vector-icons' by Joel Arvidsson.
@@ -2162,7 +2129,7 @@ This product contains 'react-native-video' by Brent Vatne.
A <Video /> element for react-native
* HOMEPAGE:
* https://github.com/react-native-community/react-native-video#readme
* https://github.com/brentvatne/react-native-video#readme
* LICENSE: MIT
@@ -2192,7 +2159,7 @@ SOFTWARE.
## react-native-webview
This product contains a modified version of 'react-native-webview' by Jamon Holmgren.
This product contains 'react-native-webview' by Jamon Holmgren.
React Native WebView component for iOS, Android, and Windows 10 (coming soon)
@@ -2591,7 +2558,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

@@ -1,4 +1,4 @@
Please make sure you've read the [pull request](https://developers.mattermost.com/contribute/getting-started/contribution-checklist/) section of our [code contribution guidelines](https://developers.mattermost.com/contribute/getting-started/).
Please make sure you've read the [pull request](http://docs.mattermost.com/developer/contribution-guide.html#preparing-a-pull-request) section of our [code contribution guidelines](http://docs.mattermost.com/developer/contribution-guide.html).
When filling in a section please remove the help text and the above text.

View File

@@ -19,20 +19,20 @@ We plan on releasing monthly updates with new features - check the [changelog](h
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)
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [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
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
- Device information
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
### Contribute Code
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted]
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues) 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,17 @@ def enableProguardInReleaseBuilds = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
pickFirst '**/libjsc.so'
pickFirst '**/libc++_shared.so'
}
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 215
versionName "1.21.2"
versionCode 175
versionName "1.16.0"
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
abiFilters "armeabi-v7a", "x86"
}
}
@@ -146,7 +136,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 +160,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 +168,6 @@ android {
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
repositories {
@@ -194,31 +179,21 @@ repositories {
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
if (details.requested.name == 'play-services-gcm') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '16.0.0'
}
}
}
}
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'
implementation 'com.android.support:percent:28.0.0'
implementation "com.google.firebase:firebase-messaging:17.3.0"
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:percent:27.1.1'
implementation "com.facebook.react:react-native:+" // From node_modules
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
@@ -239,10 +214,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'
@@ -258,5 +232,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

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" />
@@ -39,13 +37,12 @@
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />
<receiver android:name=".NotificationReplyBroadcastReceiver"
<service android:name=".NotificationReplyService"
android:enabled="true"
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"
@@ -56,7 +53,7 @@
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- for sharing-->
// for sharing
<data android:mimeType="*/*" />
</intent-filter>
</activity>

View File

@@ -1,7 +1,6 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
/**
* ResolvePromise: Helper class that abstracts boilerplate
@@ -17,41 +16,16 @@ public class ResolvePromise implements Promise {
}
@Override
public void reject(String code, WritableMap map) {
}
@Override
public void reject(String code, Throwable e) {
}
@Override
public void reject(Throwable e, WritableMap map) {
}
@Override
public void reject(String code, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e) {
}
@Override
public void reject(String code, String message, WritableMap map) {
}
@Override
public void reject(String message) {

View File

@@ -1,16 +1,11 @@
package com.mattermost.rnbeta;
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.app.NotificationChannel;
import android.content.Intent;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
@@ -19,14 +14,15 @@ import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.provider.Settings.System;
import android.util.Log;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
@@ -35,9 +31,12 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
import android.util.Log;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification {
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
public static final String NOTIFICATION_ID = "notificationId";
@@ -96,13 +95,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);
@@ -120,12 +113,6 @@ public class CustomPushNotification extends PushNotification {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
if (!"clear".equals(type)) {
String senderName = getSenderName(data.getString("sender_name"), 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);
}
@@ -168,40 +155,25 @@ public class CustomPushNotification extends PushNotification {
String packageName = mContext.getPackageName();
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
channel.setShowBadge(true);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification.setChannelId(CHANNEL_ID);
}
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 +183,15 @@ public class CustomPushNotification extends PushNotification {
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
String message = bundle.getString("message");
String subText = bundle.getString("subText");
String numberString = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
Bundle b = bundle.getBundle("userInfo");
if (b == null) {
b = new Bundle();
@@ -241,80 +222,67 @@ public class CustomPushNotification extends PushNotification {
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
}
if (badge != null) {
int badgeCount = Integer.parseInt(badge);
CustomPushNotification.badgeCount = badgeCount;
notification.setNumber(badgeCount);
if (numberString != null) {
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
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);
}
if (title != null && (!title.startsWith("@") || channelName != senderName)) {
messagingStyle
.setConversationTitle(title);
}
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
list.add(bundle);
}
int listCount = list.size() - 1;
for (int i = listCount; i >= 0; i--) {
Bundle data = list.get(i);
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);
}
}
int numMessages = getMessageCountInChannel(channelId);
notification
.setContentIntent(intent)
.setGroupSummary(true)
.setStyle(messagingStyle)
.setSmallIcon(smallIconResId)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setBadgeIconType(Notification.BADGE_ICON_SMALL);
if (numMessages == 1) {
notification
.setContentTitle(title)
.setContentText(message)
.setStyle(new Notification.BigTextStyle()
.bigText(message));
} else {
String summaryTitle = null;
if (version != null && version.equals("v2")) {
summaryTitle = String.format("(%d) %s", numMessages, title);
} else {
summaryTitle = String.format("%s (%d)", title, numMessages);
}
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
}
if (version != null && version.equals("v2")) {
style.addLine(message);
}
for (Bundle data : list) {
String msg = data.getString("message");
if (msg != message) {
style.addLine(data.getString("message"));
}
}
if (version != null && version.equals("v2")) {
notification
.setContentTitle(summaryTitle)
.setContentText(message)
.setStyle(style);
} else {
style.setBigContentTitle(message)
.setSummaryText(String.format("+%d more", (numMessages - 1)));
notification.setStyle(style)
.setContentTitle(summaryTitle);
}
}
// Let's add a delete intent when the notification is dismissed
@@ -328,11 +296,17 @@ public class CustomPushNotification extends PushNotification {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
Intent replyIntent = new Intent(mContext, NotificationReplyService.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent replyPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
replyPendingIntent = PendingIntent.getForegroundService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
} else {
replyPendingIntent = PendingIntent.getService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
@@ -354,6 +328,10 @@ public class CustomPushNotification extends PushNotification {
notification.setLargeIcon(largeIconBitmap);
}
if (subText != null) {
notification.setSubText(subText);
}
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (soundUri != "none") {
@@ -400,28 +378,4 @@ 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("@")) {
return channelName;
}
String name = message.split(":")[0];
if (name != message) {
return name;
}
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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
super("notificationDismissService");
}
@Override

View File

@@ -1,163 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
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;
public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
private Context mContext;
private Bundle bundle;
private NotificationManager notificationManager;
@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 KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
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[] 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);
}
}
});
}
}
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;
}
if (token == null || serverUrl == null) {
onReplyFailed(notificationManager, notificationId, channelId);
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildReplyPost(channelId, rootId, message.toString());
Log.i("ReactNative", String.format("JSON STRING %s", json));
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(String.format("%s/api/v4/posts", serverUrl.replaceAll("/$", "")))
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i("ReactNative", String.format("Reply with message %s FAILED exception %s", message, e.getMessage()));
onReplyFailed(notificationManager, notificationId, channelId);
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationManager, notificationId, channelId);
Log.i("ReactNative", String.format("Reply with message %s", message));
} else {
Log.i("ReactNative", String.format("Reply with message %s FAILED status %s BODY %s", message, response.code(), response.body().string()));
onReplyFailed(notificationManager, notificationId, channelId);
}
}
});
}
protected String buildReplyPost(String channelId, String rootId, String message) {
return "{"
+ "\"channel_id\": \"" + channelId + "\","
+ "\"message\": \"" + message + "\","
+ "\"root_id\": \"" + rootId + "\""
+ "}";
}
protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) {
String CHANNEL_ID = "Reply job";
Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Message failed to send.")
.setSmallIcon(smallIconResId)
.build();
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
notificationManager.notify(notificationId, notification);
}
protected void onReplySuccess(NotificationManager notificationManager, int notificationId, String channelId) {
notificationManager.cancel(notificationId);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

View File

@@ -0,0 +1,81 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
public void onCreate() {
super.onCreate();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Context mContext = this.getApplicationContext();
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
String CHANNEL_ID = "Reply job";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Replying to message")
.setContentText(packageName)
.setSmallIcon(smallIconResId)
.build();
startForeground(1, notification);
}
}
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
String channelId = bundle.getString("channel_id");
bundle.putCharSequence("text", message);
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
MainApplication app = (MainApplication) this.getApplication();
app.replyFromPushNotification = true;
Log.i("ReactNative", "Replying service");
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),
5000);
}
return null;
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

View File

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

View File

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

View File

@@ -205,12 +205,8 @@ public class RealPathUtil {
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
try {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
} catch (Exception e) {
return "application/octet-stream";
}
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
}
public static void deleteTempFiles(final File dir) {

View File

@@ -75,12 +75,9 @@ 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")) {
if (data != null) {
ReadableArray files = data.getArray("files");
String serverUrl = data.getString("url");
String token = data.getString("token");
@@ -148,19 +145,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : uris) {
@@ -170,15 +165,12 @@ public class ShareModule extends ReactContextBaseJavaModule {
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
} else {
type = "application/octet-stream";
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
}

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

@@ -2,19 +2,19 @@
buildscript {
ext {
buildToolsVersion = "28.0.3"
buildToolsVersion = "27.0.3"
minSdkVersion = 24
compileSdkVersion = 28
targetSdkVersion = 28
supportLibVersion = "28.0.0"
compileSdkVersion = 27
targetSdkVersion = 26
supportLibVersion = "27.1.1"
}
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.google.gms:google-services:3.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -49,3 +49,9 @@ allprojects {
}
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.4'
distributionUrl = distributionUrl.replace("bin", "all")
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

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

View File

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

View File

@@ -8,33 +8,18 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
getChannelByNameAndTeamName,
markChannelAsRead,
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
selectChannel,
leaveChannel as serviceLeaveChannel,
} from 'mattermost-redux/actions/channels';
import {
getPosts,
getPostsBefore,
getPostsSince,
getPostThread,
} from 'mattermost-redux/actions/posts';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {
getChannel,
getCurrentChannelId,
getMyChannelMember,
getRedirectChannelNameForTeam,
getChannelsNameMapInTeam,
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import telemetry from 'app/telemetry';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {
getChannelByName,
@@ -42,7 +27,6 @@ import {
getUserIdFromChannelName,
isDirectChannel,
isGroupChannel,
getChannelByName as getChannelByNameSelector,
} from 'mattermost-redux/utils/channel_utils';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
@@ -174,8 +158,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 +170,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
if (received?.order) {
if (received) {
const count = received.order.length;
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
actions.push({
@@ -214,7 +198,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 +249,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);
}
};
}
@@ -298,7 +282,8 @@ export function selectInitialChannel(teamId) {
lastChannel &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
dispatch(handleSelectChannel(lastChannelId));
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
}
@@ -329,7 +314,9 @@ export function selectPenultimateChannel(teamId) {
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
dispatch(setChannelLoading(true));
dispatch(setChannelDisplayName(lastChannel.display_name));
dispatch(handleSelectChannel(lastChannelId));
dispatch(markChannelAsRead(lastChannelId));
return;
}
@@ -339,48 +326,39 @@ 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.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;
}
if (channelId) {
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
dispatch(markChannelAsRead(channelId));
}
};
}
export function handleSelectChannel(channelId, fromPushNotification = false) {
export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const state = getState();
const channel = getChannel(state, channelId);
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
const sameChannel = channelId === currentChannelId;
const member = getMyChannelMember(state, channelId);
const {currentTeamId} = getState().entities.teams;
dispatch(setLoadMorePostsVisible(true));
// If the app is open from push notification, we already fetched the posts.
if (!fromPushNotification) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
selectChannel(channelId),
setChannelDisplayName(channel.display_name),
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
@@ -391,34 +369,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
teamId: currentTeamId,
channelId,
},
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel,
member,
},
]));
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
}
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
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 {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
const currentChannelId = getCurrentChannelId(state);
if (channel && currentChannelId !== channel.id) {
dispatch(handleSelectChannel(channel.id));
}
};
}
@@ -443,13 +394,6 @@ export function insertToDraft(value) {
};
}
export function markChannelViewedAndRead(channelId, previousChannelId, markOnServer = true) {
return (dispatch) => {
dispatch(markChannelAsRead(channelId, previousChannelId, markOnServer));
dispatch(markChannelAsViewed(channelId, previousChannelId));
};
}
export function toggleDMChannel(otherUserId, visible, channelId) {
return async (dispatch, getState) => {
const state = getState();
@@ -467,7 +411,7 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
value: Date.now().toString(),
}];
dispatch(savePreferences(currentUserId, dm));
savePreferences(currentUserId, dm)(dispatch, getState);
};
}
@@ -483,7 +427,7 @@ export function toggleGMChannel(channelId, visible) {
value: visible,
}];
dispatch(savePreferences(currentUserId, gm));
savePreferences(currentUserId, gm)(dispatch, getState);
};
}
@@ -492,9 +436,9 @@ export function closeDMChannel(channel) {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
dispatch(toggleDMChannel(channel.teammate_id, 'false'));
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
dispatch(selectInitialChannel(state.entities.teams.currentTeamId));
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
}
@@ -538,17 +482,11 @@ export function leaveChannel(channel, reset = false) {
await dispatch(selectDefaultChannel(currentTeamId));
}
await dispatch(serviceLeaveChannel(channel.id));
await serviceLeaveChannel(channel.id)(dispatch, getState);
};
}
export function setChannelLoading(loading = true) {
if (loading) {
telemetry.start(['channel:loading']);
} else {
telemetry.end(['channel:loading']);
}
return {
type: ViewTypes.SET_CHANNEL_LOADER,
loading,
@@ -577,7 +515,7 @@ export function setChannelDisplayName(displayName) {
}
// Returns true if there are more posts to load
export function increasePostVisibility(channelId, postId) {
export function increasePostVisibility(channelId, focusedPostId) {
return async (dispatch, getState) => {
const state = getState();
const {loadingPosts, postVisibility} = state.views.channel;
@@ -587,28 +525,22 @@ export function increasePostVisibility(channelId, postId) {
return true;
}
if (!postId) {
// No posts are visible, so the channel is empty
return true;
}
// Check if we already have the posts that we want to show
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (!focusedPostId) {
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
return true;
return true;
}
}
telemetry.reset();
telemetry.start(['posts:loading']);
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
@@ -616,8 +548,14 @@ export function increasePostVisibility(channelId, postId) {
});
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
const page = Math.floor(currentPostVisibility / pageSize);
const result = await retryGetPostsAction(getPostsBefore(channelId, postId, 0, pageSize), dispatch, getState);
let result;
if (focusedPostId) {
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
} else {
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
}
const actions = [{
type: ViewTypes.LOADING_POSTS,
@@ -626,7 +564,7 @@ export function increasePostVisibility(channelId, postId) {
}];
let hasMorePost = false;
if (result?.order) {
if (result) {
const count = result.order.length;
hasMorePost = count >= pageSize;
@@ -646,9 +584,6 @@ export function increasePostVisibility(channelId, postId) {
}
dispatch(batchActions(actions));
telemetry.end(['posts:loading']);
telemetry.save();
return hasMorePost;
};
}

View File

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

View File

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

View File

@@ -6,17 +6,6 @@ import {createChannel} from 'mattermost-redux/actions/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {cleanUpUrlable} from 'mattermost-redux/utils/channel_utils';
import {generateId} from 'mattermost-redux/utils/helpers';
export function generateChannelNameFromDisplayName(displayName) {
let name = cleanUpUrlable(displayName);
if (name === '') {
name = generateId();
}
return name;
}
export function handleCreateChannel(displayName, purpose, header, type) {
return async (dispatch, getState) => {
@@ -25,17 +14,17 @@ 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,
type,
};
const {data} = await dispatch(createChannel(channel, currentUserId));
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
if (data && data.id) {
dispatch(setChannelDisplayName(displayName));
dispatch(handleSelectChannel(data.id));
handleSelectChannel(data.id)(dispatch, getState);
}
};
}

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,14 +7,11 @@ import {getSessions} from 'mattermost-redux/actions/users';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {app} from 'app/mattermost';
import PushNotifications from 'app/push_notifications';
import {getDeviceTimezone} from 'app/utils/timezone';
import {setCSRFFromCookie} from 'app/utils/security';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
@@ -44,7 +41,6 @@ export function handleSuccessfulLogin() {
const deviceToken = state.entities.general.deviceToken;
const currentUserId = getCurrentUserId(state);
await setCSRFFromCookie(url);
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
@@ -58,11 +54,11 @@ export function handleSuccessfulLogin() {
url,
token,
},
});
}, getState);
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
@@ -71,46 +67,31 @@ export function handleSuccessfulLogin() {
};
}
export function scheduleExpiredNotification(intl) {
return (dispatch, getState) => {
export function getSession() {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
const {deviceToken} = state.entities.general;
const message = intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
});
// Once the user logs in we are going to wait for 10 seconds
// before retrieving the session that belongs to this device
// to ensure that we get the actual session without issues
// then we can schedule the local notification for the session expired
setTimeout(async () => {
let sessions;
try {
sessions = await dispatch(getSessions(currentUserId));
} catch (e) {
console.warn('Failed to get current session', e); // eslint-disable-line no-console
return;
}
if (!currentUserId || !deviceToken) {
return 0;
}
if (!Array.isArray(sessions.data)) {
return;
}
let sessions;
try {
sessions = await dispatch(getSessions(currentUserId));
} catch (e) {
console.warn('Failed to get current session', e); // eslint-disable-line no-console
return 0;
}
const session = sessions.data.find((s) => s.device_id === deviceToken);
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
if (!Array.isArray(sessions.data)) {
return 0;
}
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message,
userInfo: {
localNotification: true,
},
});
}
}, 10000);
const session = sessions.data.find((s) => s.device_id === deviceToken);
return session && session.expires_at ? session.expires_at : 0;
};
}
@@ -118,5 +99,5 @@ export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
scheduleExpiredNotification,
getSession,
};

View File

@@ -17,21 +17,6 @@ jest.mock('app/mattermost', () => ({
},
}));
jest.mock('react-native-cookies', () => ({
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
openURL: jest.fn(),
canOpenURL: jest.fn(),
getInitialURL: jest.fn(),
get: () => Promise.resolve(({
res: {
MMCSRF: {
value: 'the cookie',
},
},
})),
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Login', () => {

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,18 +1,20 @@
// 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 {fetchMyChannelsAndMembers, markChannelAsRead} 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';
import {recordTime} from 'app/utils/segment';
import {handleSelectChannel} from 'app/actions/views/channel';
import {
handleSelectChannel,
setChannelDisplayName,
} from 'app/actions/views/channel';
export function startDataCleanup() {
return async (dispatch, getState) => {
@@ -47,12 +49,12 @@ export function loadConfigAndLicense() {
};
}
export function loadFromPushNotification(notification, startAppFromPushNotification) {
export function loadFromPushNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {channels} = state.entities.channels;
const {currentChannelId, channels} = state.entities.channels;
let channelId = '';
let teamId = currentTeamId;
@@ -84,7 +86,15 @@ export function loadFromPushNotification(notification, startAppFromPushNotificat
dispatch(selectTeam({id: teamId}));
}
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
// mark channel as read
dispatch(markChannelAsRead(channelId, channelId === currentChannelId ? null : currentChannelId, false));
if (channelId !== currentChannelId) {
// when the notification is from a channel other than the current channel
dispatch(markChannelAsRead(channelId, currentChannelId, false));
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
}
};
}
@@ -111,7 +121,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,23 +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 {setDeepLinkURL} from './root';
const mockStore = configureStore([thunk]);
describe('Actions.Views.Root', () => {
const store = mockStore();
test('should set deep link URL', async () => {
const url = 'https://test-url.com/team-name/pl/pl-id';
const action = {
type: 'SET_DEEP_LINK_URL',
url,
};
await store.dispatch(setDeepLinkURL(url));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -1,16 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {TeamTypes} from 'mattermost-redux/action_types';
import {batchActions} from 'redux-batched-actions';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getMyTeams} from 'mattermost-redux/actions/teams';
import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {NavigationTypes} from 'app/constants';
import {selectFirstAvailableTeam} from 'app/utils/teams';
export function handleTeamChange(teamId) {
import {setChannelDisplayName} from './channel';
export function handleTeamChange(teamId, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
@@ -18,7 +24,19 @@ export function handleTeamChange(teamId) {
return;
}
dispatch({type: TeamTypes.SELECT_TEAM, data: teamId});
const actions = [setChannelDisplayName(''), {type: TeamTypes.SELECT_TEAM, data: teamId}];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
const lastChannelId = lastChannels[0] || '';
const currentChannelId = getCurrentChannelId(state);
markChannelAsViewed(currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
}
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState);
};
}

View File

@@ -2,13 +2,18 @@
// See LICENSE.txt for license information.
import {UserTypes} from 'mattermost-redux/action_types';
import {getStatus, getStatusesByIds, startPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
import {General} from 'mattermost-redux/constants';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
export function setCurrentUserStatusOffline() {
export function setCurrentUserStatus(isOnline) {
return (dispatch, getState) => {
const currentUserId = getCurrentUserId(getState());
if (isOnline) {
return dispatch(getStatus(currentUserId));
}
return dispatch({
type: UserTypes.RECEIVED_STATUS,
data: {
@@ -18,3 +23,16 @@ export function setCurrentUserStatusOffline() {
});
};
}
export function initUserStatuses() {
return (dispatch, getState) => {
const {statuses} = getState().entities.users || {};
const userIds = Object.keys(statuses);
if (userIds.length) {
dispatch(getStatusesByIds(userIds));
}
dispatch(startPeriodicStatusUpdates());
};
}

View File

@@ -7,7 +7,7 @@ import thunk from 'redux-thunk';
import {UserTypes} from 'mattermost-redux/action_types';
import {General} from 'mattermost-redux/constants';
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
import {initUserStatuses, setCurrentUserStatus} from 'app/actions/views/user';
const mockStore = configureStore([thunk]);
@@ -44,7 +44,28 @@ describe('Actions.Views.User', () => {
},
};
await store.dispatch(setCurrentUserStatusOffline());
await store.dispatch(setCurrentUserStatus(false));
expect(store.getActions()).toEqual([action]);
});
test('should fetch the current user status from the server', async () => {
const action = {
type: 'MOCK_GET_STATUS',
args: ['current-user-id'],
};
await store.dispatch(setCurrentUserStatus(true));
expect(store.getActions()).toEqual([action]);
});
test('should initialize the periodic status updates and get the current user statuses', () => {
const actionStatusByIds = {
type: 'MOCK_GET_STATUS_BY_IDS',
args: [['current-user-id', 'another-user-id1', 'another-user-id2']],
};
const actionPeriodicUpdates = {type: 'MOCK_PERIODIC_STATUS_UPDATES'};
store.dispatch(initUserStatuses());
expect(store.getActions()).toEqual([actionStatusByIds, actionPeriodicUpdates]);
});
});

View File

@@ -2,11 +2,9 @@
// See LICENSE.txt for license information.
/* eslint-disable global-require*/
import {Linking, NativeModules, Platform, Text} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {setDeviceToken} from 'mattermost-redux/actions/general';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@@ -19,7 +17,6 @@ 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;
@@ -32,7 +29,6 @@ export default class App {
// Usage: app.js
this.shouldRelaunchWhenActive = false;
this.inBackgroundSince = null;
this.previousAppState = null;
// Usage: screen/entry.js
this.startAppFromPushNotification = false;
@@ -55,6 +51,14 @@ export default class App {
this.token = null;
this.url = null;
// Load polyfill for iOS 9
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS < 10) {
require('@babel/polyfill');
}
}
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
@@ -128,15 +132,12 @@ export default class App {
// if for any case the url and the token aren't valid proceed with re-hydration
if (url && url !== 'undefined' && token && token !== 'undefined') {
const {dispatch} = store;
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
dispatch(setDeviceToken(deviceToken));
Client4.setUrl(url);
Client4.setToken(token);
await setCSRFFromCookie(url);
} else {
this.waitForRehydration = true;
}
@@ -201,11 +202,10 @@ export default class App {
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
this.token = token;
this.url = url;
if (this.waitForRehydration) {
this.waitForRehydration = false;
this.token = token;
this.url = url;
}
// Only save to keychain if the url and token are set
@@ -314,6 +314,7 @@ export default class App {
break;
}
this.setStartAppFromPushNotification(false);
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,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AttachmentButton should match snapshot 1`] = `
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"height": 34,
"justifyContent": "center",
"width": 45,
}
}
>
<Icon
allowFontScaling={false}
color="rgba(61,60,64,0.9)"
name="md-add"
size={30}
style={
Object {
"marginTop": 2,
}
}
/>
</TouchableOpacity>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ import {
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import BotTag from 'app/components/bot_tag';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
@@ -20,15 +19,9 @@ export default class AtMentionItem extends PureComponent {
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
isBot: PropTypes.bool,
theme: PropTypes.object.isRequired,
};
static defaultProps = {
firstName: '',
lastName: '',
};
completeMention = () => {
const {onPress, username} = this.props;
onPress(username);
@@ -41,7 +34,6 @@ export default class AtMentionItem extends PureComponent {
userId,
username,
theme,
isBot,
} = this.props;
const style = getStyleFromTheme(theme);
@@ -62,10 +54,6 @@ export default class AtMentionItem extends PureComponent {
/>
</View>
<Text style={style.rowUsername}>{`@${username}`}</Text>
<BotTag
show={isBot}
theme={theme}
/>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import {
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import BotTag from 'app/components/bot_tag';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -19,7 +18,6 @@ export default class ChannelMentionItem extends PureComponent {
displayName: PropTypes.string,
name: PropTypes.string,
type: PropTypes.string,
isBot: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
};
@@ -40,7 +38,6 @@ export default class ChannelMentionItem extends PureComponent {
name,
theme,
type,
isBot,
} = this.props;
const style = getStyleFromTheme(theme);
@@ -56,10 +53,6 @@ export default class ChannelMentionItem extends PureComponent {
style={style.row}
>
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
<BotTag
show={isBot}
theme={theme}
/>
</TouchableOpacity>
);
}

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