Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
753a1b52ce | ||
|
|
4e83c1f944 | ||
|
|
50082e327b | ||
|
|
920e46f9a1 | ||
|
|
fac489a5d0 | ||
|
|
cbb9a2b749 | ||
|
|
b382f81cf6 | ||
|
|
66cd2a98ac | ||
|
|
317e3b14ab | ||
|
|
87af5404ae | ||
|
|
4f3679e09d | ||
|
|
976a29ed29 | ||
|
|
ffef7e2bcf | ||
|
|
e5e741840e | ||
|
|
83ec54b8c8 | ||
|
|
226bfa8dd2 | ||
|
|
1ebf6fc0c2 | ||
|
|
d40b1da1d0 | ||
|
|
b24c97b7e9 | ||
|
|
5d61ae3342 | ||
|
|
ccc33cbf39 | ||
|
|
43a0d25636 | ||
|
|
860d4c582f | ||
|
|
c930bf85af | ||
|
|
97e2647eae | ||
|
|
24ff55f4c0 | ||
|
|
95fb8a9348 | ||
|
|
2ded5182f3 | ||
|
|
83b0cfb9ba | ||
|
|
d29998957b | ||
|
|
7e182f6022 | ||
|
|
7fa7cf4364 |
@@ -67,4 +67,4 @@ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
[version]
|
||||
^0.92.0
|
||||
^0.78.0
|
||||
|
||||
2
.gitignore
vendored
@@ -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
|
||||
|
||||
140
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
279
NOTICE.txt
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
16
README.md
@@ -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 you’re 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
|
||||
|
||||
@@ -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,26 +107,17 @@ def enableProguardInReleaseBuilds = false
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'lib/x86_64/libjsc.so'
|
||||
pickFirst 'lib/arm64-v8a/libjsc.so'
|
||||
}
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 198
|
||||
versionName "1.20.0"
|
||||
versionCode 176
|
||||
versionName "1.16.1"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -145,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 {
|
||||
@@ -169,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 =
|
||||
@@ -177,11 +168,6 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -194,19 +180,10 @@ configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'android-jsc') {
|
||||
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r241213'
|
||||
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
|
||||
}
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,9 +192,8 @@ configurations.all {
|
||||
dependencies {
|
||||
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')
|
||||
@@ -238,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'
|
||||
@@ -257,5 +232,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,7 +37,7 @@
|
||||
<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
|
||||
@@ -55,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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +45,6 @@ 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 {
|
||||
@@ -62,14 +52,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
public Boolean replyFromPushNotification = false;
|
||||
|
||||
public long APP_START_TIME;
|
||||
|
||||
public long RELOAD;
|
||||
public long CONTENT_APPEARED;
|
||||
|
||||
public long PROCESS_PACKAGES_START;
|
||||
public long PROCESS_PACKAGES_END;
|
||||
|
||||
@Override
|
||||
public boolean isDebug() {
|
||||
return BuildConfig.DEBUG;
|
||||
@@ -102,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()
|
||||
);
|
||||
@@ -130,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
|
||||
@@ -157,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -58,24 +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 quitApp() {
|
||||
getCurrentActivity().finish();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,155 +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) {
|
||||
mContext = context;
|
||||
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
|
||||
|
||||
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@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 rootId = bundle.getString("post_id");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 630 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 508 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 33 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 971 B |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -2,19 +2,19 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "28.0.3"
|
||||
buildToolsVersion = "27.0.3"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 28
|
||||
compileSdkVersion = 27
|
||||
targetSdkVersion = 26
|
||||
supportLibVersion = "28.0.0"
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,9 +11,7 @@ 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, isTimezoneEnabled} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -43,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);
|
||||
@@ -57,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: {}});
|
||||
}
|
||||
@@ -70,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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,5 +99,5 @@ export default {
|
||||
handleLoginIdChanged,
|
||||
handlePasswordChanged,
|
||||
handleSuccessfulLogin,
|
||||
scheduleExpiredNotification,
|
||||
getSession,
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
15
app/app.js
@@ -2,8 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable global-require*/
|
||||
import {Linking, NativeModules, Platform, Text} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
|
||||
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
|
||||
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
@@ -18,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;
|
||||
|
||||
@@ -31,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;
|
||||
@@ -54,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);
|
||||
|
||||
@@ -133,7 +138,6 @@ export default class App {
|
||||
this.url = url;
|
||||
Client4.setUrl(url);
|
||||
Client4.setToken(token);
|
||||
await setCSRFFromCookie(url);
|
||||
} else {
|
||||
this.waitForRehydration = true;
|
||||
}
|
||||
@@ -310,6 +314,7 @@ export default class App {
|
||||
break;
|
||||
}
|
||||
|
||||
this.setStartAppFromPushNotification(false);
|
||||
this.setAppStarted(true);
|
||||
}
|
||||
}
|
||||
|
||||
6
app/components/__mocks__/react.js
vendored
@@ -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};
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -53,6 +53,5 @@ exports[`profile_picture_button should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
uploadFiles={[MockFunction]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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'});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
}];
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -44,11 +44,6 @@ export default class ChannelMention extends PureComponent {
|
||||
static defaultProps = {
|
||||
isSearch: false,
|
||||
value: '',
|
||||
publicChannels: [],
|
||||
privateChannels: [],
|
||||
directAndGroupMessages: [],
|
||||
myChannels: [],
|
||||
otherChannels: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
@@ -13,25 +11,14 @@ import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
|
||||
|
||||
import ChannelMentionItem from './channel_mention_item';
|
||||
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
const displayName = getChannelNameForSearchAutocomplete(state, ownProps.channelId);
|
||||
|
||||
let isBot = false;
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const teammate = getUser(state, channel.teammate_id);
|
||||
if (teammate && teammate.is_bot) {
|
||||
isBot = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayName,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
isBot,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,4 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);
|
||||
export default connect(mapStateToProps, null, null, {withRef: true})(Autocomplete);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
export default class BotTag extends PureComponent {
|
||||
static defaultProps = {
|
||||
show: true,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.show) {
|
||||
return null;
|
||||
}
|
||||
const style = createStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<FormattedText
|
||||
id='post_info.bot'
|
||||
defaultMessage='BOT'
|
||||
style={style.bot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
bot: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 2,
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import Icon from 'app/components/vector_icon';
|
||||
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
export default class ChannelIcon extends React.PureComponent {
|
||||
@@ -27,7 +26,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -49,7 +47,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
theme,
|
||||
type,
|
||||
isArchived,
|
||||
isBot,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -86,15 +83,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='archive'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (isBot) {
|
||||
icon = (
|
||||
<Icon
|
||||
name='robot'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}, style.iconBot]}
|
||||
type='fontawesome5'
|
||||
/>
|
||||
);
|
||||
} else if (hasDraft) {
|
||||
@@ -102,7 +90,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='pencil'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.OPEN_CHANNEL) {
|
||||
@@ -110,7 +97,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
@@ -118,7 +104,6 @@ export default class ChannelIcon extends React.PureComponent {
|
||||
<Icon
|
||||
name='lock'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
type='fontawesome'
|
||||
/>
|
||||
);
|
||||
} else if (type === General.GM_CHANNEL) {
|
||||
@@ -192,9 +177,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
iconInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
iconBot: {
|
||||
marginLeft: -5,
|
||||
},
|
||||
groupBox: {
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'center',
|
||||
|
||||