Compare commits

...

80 Commits

Author SHA1 Message Date
Elias Nahum
daa0c97e3f Bump app build number to 182 (#2661) 2019-03-19 21:20:34 -03:00
Elias Nahum
badaaef592 iOS Share Extension fix post and dm/gm list (#2659)
* MM-14629 DMs and GMs for the iOS extension to match sidebar

* MM-14631 ios extension logging and fix race condition to post after uploading
2019-03-19 21:16:10 -03:00
Elias Nahum
96571c0f9c MM-14630 Mark channel as read and viewed when opening brought from the background (#2658) 2019-03-19 19:59:36 -03:00
Carlos Tadeu Panato Junior
ebe776e8b4 Bump app build number to 181 (#2651) 2019-03-16 11:12:26 +01:00
Saturnino Abril
242c6cc171 fix email setting to "Never" when first saved (hitting back button on iOS) without actually changing the selection (#2650) 2019-03-16 03:06:40 +08:00
Saturnino Abril
e73aa8662f fix channel mention badge and posts after opening the app from push notification (which is initially at closed state) (#2648) 2019-03-16 00:06:15 +08:00
Saturnino Abril
aa80adfacc fix relative URL permalink (#2649) 2019-03-15 23:29:20 +08:00
Elias Nahum
2f7a12bc16 translations PR 20190314 (#2646) 2019-03-14 13:41:58 -03:00
Carlos Tadeu Panato Junior
779306ae6f Bump app build number to 180 (#2645) 2019-03-14 13:03:22 +01:00
Saturnino Abril
c4d4009fad Fix default behavior when enabling/disabling email batching (#2642) 2019-03-14 03:02:33 +08:00
Saturnino Abril
35efa47b35 fix channel mention when app is started from push notification (#2638) 2019-03-11 15:36:16 -04:00
Saturnino Abril
c7dc992ac0 do not save email interval preference on cancel on Android device (#2633) 2019-03-09 13:29:14 +08:00
Elias Nahum
d22422840b Bump app build number to 178 (#2630) 2019-03-05 12:21:34 -03:00
Elias Nahum
cc3ce0e509 Wait for VPN connection only when appconfig flag is set (#2627) 2019-03-05 12:02:55 -03:00
Elias Nahum
fbd5c34c5e Add target to Mattermost and MattermostShare (#2626) 2019-03-05 12:01:12 -03:00
Elias Nahum
9485d4e88d Set the correct channel display name when switching teams (#2625) 2019-03-05 11:59:27 -03:00
Elias Nahum
2c5252e386 Fix regression when sharing files with spaces in the filename (#2624) 2019-03-05 11:55:44 -03:00
Saturnino Abril
aa7054ee1e fix enabling post icon override (#2611) 2019-03-05 19:54:20 +08:00
Saturnino Abril
eb0fd4f3ad fix undefined site URL when copying permalink (#785) 2019-03-05 19:45:28 +08:00
Elias Nahum
490e2e550e Fix checkLoginResponse for MFA (#2623) 2019-03-05 09:09:53 +08:00
Saturnino Abril
fe46802aed fix intermittent crash when selecting a link (#2620) (#2622) 2019-03-05 08:54:03 +08:00
Saturnino Abril
9253134612 fix default email interval from never to immediate (#2614) 2019-03-04 15:53:49 -05:00
Saturnino Abril
2f8e4116a1 [MM-14238] Add make command to generate unsigned x86_64 build for VM/simulator (#2597)
* add make command to generate unsigned x86_64 build for VM/simulator

* updated per comment, added common function at makefile

* added start|stop_packger to start and stop phony
2019-03-02 02:24:32 +08:00
Daniel Schalla
5f0a045046 [MM-14081] Show MFA Prompt after Server returned error for login request (#2599)
* Show MFA Prompt after Server returned error for login request

* eslint

* Hide MFA error on first login attempt
2019-03-01 19:04:18 +01:00
Elias Nahum
f53a7c26b8 Fix crash when fresh install android when getting the fcm token (#2608) 2019-02-28 11:16:55 -08:00
Miguel Alatzar
d012bead87 Ensure data has url key before attempting upload (#2609) 2019-02-28 10:59:27 -08:00
Miguel Alatzar
7a933bef29 Check for null uri prior to getting path to shared content (#2607) 2019-02-28 10:57:48 -08:00
Elias Nahum
1dcb8f2b25 Handle pending posts when dispatching RECEIVED_NEW_POST (#2606) 2019-02-28 13:52:53 -03:00
Elias Nahum
ee501a4b50 Update sentry to fix android build (#2603) 2019-02-27 16:43:20 -03:00
Elias Nahum
830113d6b5 Bump Version to 1.17.0 Build 177 (#2602)
* Bump app build number to 177

* Bump app version number to 1.17.0
2019-02-26 23:09:53 -03:00
Miguel Alatzar
a6ad628c4f Use forked react-native-navigation with v1 patch (#2596) 2019-02-26 12:39:19 -08:00
Elias Nahum
a34ff7b247 iOS Native Share Extension (Swift) (#2575)
* iOS Native Share Extension (Swift)

* Re-arrange files

* Fix .gitignore
2019-02-26 14:32:48 -03:00
Elias Nahum
4f9d0c68b5 translations PR 20190225 (#2595) 2019-02-26 09:16:29 -03:00
Elias Nahum
cf7f92ea47 Update Android RAM Bundle (#2585)
* Update Android RAM Bundle

* Remove missed files and fix README

* rename schedule to scheduler
2019-02-25 14:19:14 -08:00
Dean Whillier
e1b6f174f9 immediately return when connection is offline (#2594) 2019-02-25 16:25:50 -05:00
Miguel Alatzar
42a4806d32 [MM-13954] Fix prop type for post reactions (#2578)
* Fix prop type for post reactions

* Update react list test
2019-02-21 21:41:08 +08:00
Sudheer
7bb6821ea6 MM-13428 Updating mm-redux hash (#2566) 2019-02-21 03:11:43 +05:30
Elias Nahum
c34a706b62 Dependencies update (#2576)
* Dependencies update

* Fix WebView

* Update to RN 0.58.5 and include the WebSocket fix

* Update react-native-video

* Fix RNFetchBlob
2019-02-20 17:35:50 -03:00
Elias Nahum
787d5e4208 Migrate Android from GCM to FCM (#2577) 2019-02-20 12:23:05 -08:00
Elias Nahum
f376d0b07b Bump app Version to 1.16.1 and Build Number to 176 (#2584)
* Bump app build number to 176

* Bump app version number to 1.16.1

* Update fastlane
2019-02-20 16:56:17 -03:00
Elias Nahum
fb75bf3916 Prevent the app from crashing when file mime type is null (#2582) 2019-02-20 16:48:35 -03:00
Saturnino Abril
a531760e64 [MM-14017] Fix whitespace between input box and post list on iOS (#2580)
* fix whitespace between input box and post list on iOS

* corrected viewPosition when pulling down the post list and add constant for thread screen

* corrected changes made to viewPosition
2019-02-19 21:22:17 -08:00
Elias Nahum
4c9ad4a424 Fix opengraph and reactions for older servers (#2581) 2019-02-19 19:06:27 -03:00
Saturnino Abril
0de0920974 [MM-13295] Make channel link clickable for user who's not member of the public channel (#2466)
* Make channel link clickable for user who's not member of the public channel

* update per review

* update per comment

* remove setChannelDisplayName
2019-02-20 01:46:37 +08:00
Elias Nahum
d69ef1b777 translations PR 20190218 (#2579) 2019-02-19 10:47:19 -03:00
Elias Nahum
883752c9f2 MM-13896 Sync channel as read between clients (including opening Push Notifications) (#2548)
* Update mattermost-redux to use latest WS changes

* Properly mark channel as read, keep the new message indicator, and fix push notification flow

* unneeded parameter in setCurrentUserStatusOffline

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

* Feedback review

* Update mm-redux ref
2019-02-16 15:11:31 -03:00
Dean Whillier
7f6243f347 [MM 13720] Fix for "No internet connection" banner not always showing (#2563)
* Add `opacity` to compopnent state …

… to trigger render updates when the property changes.

* Fix bug stopping app from updating connection status

- if the app is opened when no internet connection is present, turning on the internet with the app open will not remove the `No internet connection` message.

* style corrections
2019-02-16 14:14:47 -03:00
Elias Nahum
c47980e0f2 MM-13830 Ensure schedule of session expired notification (#2554)
* Ensure schedule of session expired notification

* feedback review

* passing intl instead of message
2019-02-16 14:13:47 -03:00
Dean Whillier
7369434048 [MM-13677] Fix to ensure unsupported unicode emoji can be rendered jumbo size (#2545)
* fixes unicode emoji not rendering jumbo

- replace inline emoji regex with npm emoji-regex library
- update notice.txt with emoji-regex license
- jumbo emoji issue automatically fixed with updated unicode emoji regex

* Add several tests specifically for 🤟emoji.
2019-02-16 14:13:12 -03:00
Dean Whillier
24a32867b1 Enable tapping on channels when jump to textfield is focused. (#2535)
* Allow tapping on channels in the filtered list …

… when the keyboard is open. This option also allows tapping on non-interactive areas to still dismiss the keyboard.

* keyboard propery updates

- change ‘on-drag’ to ‘interractive’
- change ‘handled’ to ‘always’ to be more consistent with other uses of same property

* support multiple keyboard dismiss modes
2019-02-16 14:12:37 -03:00
Elias Nahum
758743580d MM-13875 Fix user profile icon for a Webhook to follow the prop for use_user_icon (#2533) 2019-02-16 14:12:02 -03:00
Elias Nahum
920e46f9a1 Bump app build number to 175 (#2573) 2019-02-15 13:36:57 -08:00
Elias Nahum
fac489a5d0 Fix a crash when displaying the app in russian 2019-02-15 09:50:02 -03:00
Elias Nahum
cbb9a2b749 Bump app build number to 174 (#2572) 2019-02-14 13:56:33 -08:00
JoramWilander
b382f81cf6 Update mattermost-redux 2019-02-14 13:49:38 -05:00
Dean Whillier
66cd2a98ac [MM 13934] Prevent blank mention keys from crashing the app (#2570)
* don’t match blank word boundaries

* test for blank mention keys

* Check for blank mention before looking for match

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

* Update package.json

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

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

* set the image_cache siteURL from redux

* HH suggestion

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

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

* Add translation entry, change wording

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

* change max initial position from 60% to 70%

* fix broken UI of the bottom part of post options

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

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

* Fix position of dot for unread indicator in badge

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

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
assets/override
dist
build-ios
*.zip
server.PID
mattermost.keystore

View File

@@ -2,7 +2,7 @@
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: test help
@@ -91,21 +91,10 @@ 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
@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
$(call start_packager)
stop: ## Stops the React Native packager server
@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
$(call stop_packager)
check-device-ios:
@if ! [ $(shell which xcodebuild) ]; then \
@@ -181,68 +170,61 @@ 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
@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
$(call start_packager)
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(call stop_packager)
build-ios: | stop pre-build check-style ## Builds the iOS app
@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
$(call start_packager)
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(call stop_packager)
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
@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
$(call start_packager)
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(call stop_packager)
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
@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
$(call start_packager)
@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 Relase -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 Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@rm -rf build-ios/
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(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/
$(call stop_packager)
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
@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
$(call start_packager)
@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
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(call stop_packager)
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
@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
$(call start_packager)
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
@ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9
$(call stop_packager)
can-build-pr:
@if [ -z ${PR_ID} ]; then \
@@ -258,3 +240,22 @@ i18n-extract: ## Extract strings for translation from the source code
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
define start_packager
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
else \
echo React Native packager server already running; \
fi
endef
define stop_packager
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
endef

View File

@@ -20,7 +20,7 @@ Provides polyfills necessary for a full ES2015+ environment
MIT License
Copyright (c) 2014-2018 Sebastian McKenzie and other contributors
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
@@ -56,7 +56,7 @@ babel's modular runtime helpers
MIT License
Copyright (c) 2014-2018 Sebastian McKenzie and other contributors
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
@@ -310,6 +310,42 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## emoji-regex
This product contains 'emoji-regex' by Mathias Bynens.
Regular expression to match all emoji symbols (including textual representations of emoji) as per the Unicode Standard.
* HOMEPAGE:
* https://github.com/mathiasbynens/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.
@@ -641,7 +677,7 @@ SOFTWARE.
## jsc-android
This product contains 'jsc-android' by React Community.
This product contains 'jsc-android' by React Native Community.
Pre-build version of JavaScriptCore to be used by React Native apps
@@ -1468,12 +1504,12 @@ SOFTWARE.
## react-native-gesture-handler
This product contains 'react-native-gesture-handler' by kmagiera
This product contains 'react-native-gesture-handler' by Krzysztof Magiera.
Declarative API exposing platform native touch and gesture system to React Native.
Experimental implementation of a new declarative API for gesture handling in react-native
* HOMEPAGE:
* https://github.com/kmagiera/react-native-gesture-handler
* https://github.com/kmagiera/react-native-gesture-handler#readme
* LICENSE: MIT
@@ -1637,7 +1673,7 @@ This product contains 'react-native-linear-gradient' by Brent Vatne.
A <LinearGradient> element for React Native
* HOMEPAGE:
* https://github.com/react-native-community/react-native-linear-gradient
* https://github.com/react-native-community/react-native-linear-gradient#readme
* LICENSE: MIT
@@ -2005,7 +2041,7 @@ Note: An original license file for this dependency is not available. We determin
MIT License
Copyright (c) 2018 Brent Vatne
Copyright (c) 2019 Brent Vatne
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:
@@ -2022,7 +2058,7 @@ This product contains 'react-native-svg' by React Native Community.
SVG library for react-native
* HOMEPAGE:
* https://github.com/magicismight/react-native-svg#readme
* https://github.com/react-native-community/react-native-svg#readme
* LICENSE: MIT
@@ -2050,43 +2086,6 @@ 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.
@@ -2660,7 +2659,7 @@ Note: An original license file for this dependency is not available. We determin
MIT License
Copyright (c) 2018 Brian Grinstead
Copyright (c) 2019 Brian Grinstead
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:

View File

@@ -109,12 +109,17 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
packagingOptions {
pickFirst 'lib/x86_64/libjsc.so'
pickFirst 'lib/arm64-v8a/libjsc.so'
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 170
versionName "1.15.2"
versionCode 182
versionName "1.17.0"
multiDexEnabled = true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -182,8 +187,17 @@ configurations.all {
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
}
if (details.requested.name == 'play-services-gcm') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '16.0.0'
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'
}
}
}
@@ -192,8 +206,9 @@ configurations.all {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:percent:27.1.1'
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.facebook.react:react-native:+" // From node_modules
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
@@ -232,3 +247,5 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,8 @@ import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
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 {getFilesForPost} from 'mattermost-redux/actions/files';
@@ -18,8 +18,8 @@ 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 {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {getChannel, getCurrentChannelId, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {
getChannelByName,
@@ -282,8 +282,7 @@ export function selectInitialChannel(teamId) {
lastChannel &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
dispatch(handleSelectChannel(lastChannelId));
return;
}
@@ -314,9 +313,7 @@ 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;
}
@@ -326,7 +323,7 @@ export function selectPenultimateChannel(teamId) {
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const channels = getState().entities.channels.channels;
const {channels} = getState().entities.channels;
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
@@ -342,23 +339,30 @@ export function selectDefaultChannel(teamId) {
}
if (channelId) {
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
dispatch(markChannelAsRead(channelId));
}
};
}
export function handleSelectChannel(channelId) {
export function handleSelectChannel(channelId, fromPushNotification = false) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
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);
dispatch(setLoadMorePostsVisible(true));
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
// If the app is open from push notification, we already fetched the posts.
if (!fromPushNotification) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
dispatch(batchActions([
selectChannel(channelId),
setChannelDisplayName(channel.display_name),
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
@@ -369,7 +373,19 @@ export function handleSelectChannel(channelId) {
teamId: currentTeamId,
channelId,
},
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
member,
},
]));
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
}
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
@@ -394,6 +410,13 @@ 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();
@@ -411,7 +434,7 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
value: Date.now().toString(),
}];
savePreferences(currentUserId, dm)(dispatch, getState);
dispatch(savePreferences(currentUserId, dm));
};
}
@@ -427,7 +450,7 @@ export function toggleGMChannel(channelId, visible) {
value: visible,
}];
savePreferences(currentUserId, gm)(dispatch, getState);
dispatch(savePreferences(currentUserId, gm));
};
}
@@ -436,9 +459,9 @@ export function closeDMChannel(channel) {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
dispatch(toggleDMChannel(channel.teammate_id, 'false'));
if (channel.id === currentChannelId) {
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
dispatch(selectInitialChannel(state.entities.teams.currentTeamId));
}
};
}
@@ -482,7 +505,7 @@ export function leaveChannel(channel, reset = false) {
await dispatch(selectDefaultChannel(currentTeamId));
}
await serviceLeaveChannel(channel.id)(dispatch, getState);
await dispatch(serviceLeaveChannel(channel.id));
};
}

View File

@@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
type,
};
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
const {data} = await dispatch(createChannel(channel, currentUserId));
if (data && data.id) {
dispatch(setChannelDisplayName(displayName));
handleSelectChannel(data.id)(dispatch, getState);
dispatch(handleSelectChannel(data.id));
}
};
}

View File

@@ -11,6 +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';
export function handleLoginIdChanged(loginId) {
@@ -54,11 +55,11 @@ export function handleSuccessfulLogin() {
url,
token,
},
}, getState);
});
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
getDataRetentionPolicy()(dispatch, getState);
dispatch(getDataRetentionPolicy());
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
@@ -67,31 +68,46 @@ export function handleSuccessfulLogin() {
};
}
export function getSession() {
return async (dispatch, getState) => {
export function scheduleExpiredNotification(intl) {
return (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.',
});
if (!currentUserId || !deviceToken) {
return 0;
}
// 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;
}
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;
}
if (!Array.isArray(sessions.data)) {
return;
}
if (!Array.isArray(sessions.data)) {
return 0;
}
const session = sessions.data.find((s) => s.device_id === deviceToken);
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
const session = sessions.data.find((s) => s.device_id === deviceToken);
return session && session.expires_at ? session.expires_at : 0;
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message,
userInfo: {
localNotification: true,
},
});
}
}, 10000);
};
}
@@ -99,5 +115,5 @@ export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
getSession,
scheduleExpiredNotification,
};

View File

@@ -4,17 +4,14 @@
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {fetchMyChannelsAndMembers, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {ViewTypes} from 'app/constants';
import {recordTime} from 'app/utils/segment';
import {
handleSelectChannel,
setChannelDisplayName,
} from 'app/actions/views/channel';
import {handleSelectChannel} from 'app/actions/views/channel';
export function startDataCleanup() {
return async (dispatch, getState) => {
@@ -49,12 +46,12 @@ export function loadConfigAndLicense() {
};
}
export function loadFromPushNotification(notification) {
export function loadFromPushNotification(notification, startAppFromPushNotification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId, channels} = state.entities.channels;
const {channels} = state.entities.channels;
let channelId = '';
let teamId = currentTeamId;
@@ -86,15 +83,7 @@ export function loadFromPushNotification(notification) {
dispatch(selectTeam({id: teamId}));
}
// 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));
}
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
};
}

View File

@@ -1,22 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {TeamTypes} from 'mattermost-redux/action_types';
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';
import {setChannelDisplayName} from './channel';
export function handleTeamChange(teamId, selectChannel = true) {
export function handleTeamChange(teamId) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
@@ -24,19 +18,7 @@ export function handleTeamChange(teamId, selectChannel = true) {
return;
}
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);
dispatch({type: TeamTypes.SELECT_TEAM, data: teamId});
};
}

View File

@@ -2,18 +2,13 @@
// 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 setCurrentUserStatus(isOnline) {
export function setCurrentUserStatusOffline() {
return (dispatch, getState) => {
const currentUserId = getCurrentUserId(getState());
if (isOnline) {
return dispatch(getStatus(currentUserId));
}
return dispatch({
type: UserTypes.RECEIVED_STATUS,
data: {
@@ -23,16 +18,3 @@ export function setCurrentUserStatus(isOnline) {
});
};
}
export function initUserStatuses() {
return (dispatch, getState) => {
const {statuses} = getState().entities.users || {};
const userIds = Object.keys(statuses);
if (userIds.length) {
dispatch(getStatusesByIds(userIds));
}
dispatch(startPeriodicStatusUpdates());
};
}

View File

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

View File

@@ -314,7 +314,6 @@ export default class App {
break;
}
this.setStartAppFromPushNotification(false);
this.setAppStarted(true);
}
}

View File

@@ -28,29 +28,41 @@ exports[`Badge should match snapshot 1`] = `
style={
Object {
"alignItems": "center",
"alignSelf": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<Text
onLayout={[Function]}
<View
style={
Array [
Object {
"color": "white",
"fontSize": 14,
},
Object {
"color": "#145dbf",
"fontSize": 10,
},
Object {},
]
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"textAlignVertical": "center",
}
}
>
1
</Text>
<Text
onLayout={[Function]}
style={
Array [
Object {
"color": "white",
"fontSize": 14,
},
Object {
"color": "#145dbf",
"fontSize": 10,
},
]
}
>
1
</Text>
</View>
</View>
</View>
`;

View File

@@ -61,10 +61,10 @@ export default class AttachmentButton extends PureComponent {
};
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('photo');
return this.attachFileFromCamera('photo', 'camera');
};
attachFileFromCamera = async (mediaType) => {
attachFileFromCamera = async (mediaType, source) => {
const {formatMessage} = this.context.intl;
const options = {
quality: 0.8,
@@ -92,7 +92,7 @@ export default class AttachmentButton extends PureComponent {
},
};
const hasPhotoPermission = await this.hasPhotoPermission();
const hasPhotoPermission = await this.hasPhotoPermission(source);
if (hasPhotoPermission) {
ImagePicker.launchCamera(options, (response) => {
@@ -141,7 +141,7 @@ export default class AttachmentButton extends PureComponent {
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('video');
return this.attachFileFromCamera('video', 'camera');
};
attachVideoFromLibraryAndroid = () => {
@@ -206,11 +206,11 @@ export default class AttachmentButton extends PureComponent {
}
};
hasPhotoPermission = async () => {
hasPhotoPermission = async (source) => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check('photo');
const hasPermissionToStorage = await Permissions.check(source || 'photo');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
@@ -295,13 +295,13 @@ export default class AttachmentButton extends PureComponent {
defaultMessage: 'To upload images from your Android device, please change your permission settings.',
}),
[
grantOption,
{
text: formatMessage({
id: 'mobile.android.permission_denied_dismiss',
defaultMessage: 'Dismiss',
}),
},
grantOption,
]
);
return false;

View File

@@ -96,13 +96,30 @@ export default class Badge extends PureComponent {
renderText = () => {
const {count} = this.props;
let text = count.toString();
const extra = {};
let unreadCount = null;
let unreadIndicator = null;
if (count < 0) {
text = '•';
//the extra margin is to align to the center?
extra.marginBottom = 1;
unreadIndicator = (
<View
style={[styles.text, this.props.countStyle]}
onLayout={this.onLayout}
>
<View style={styles.verticalAlign}>
<View style={[styles.unreadIndicator, {backgroundColor: this.props.countStyle.color}]}/>
</View>
</View>
);
} else {
unreadCount = (
<View style={styles.verticalAlign}>
<Text
style={[styles.text, this.props.countStyle]}
onLayout={this.onLayout}
>
{count.toString()}
</Text>
</View>
);
}
return (
<View
@@ -110,12 +127,8 @@ export default class Badge extends PureComponent {
style={[styles.badge, this.props.style, {opacity: 0}]}
>
<View style={styles.wrapper}>
<Text
style={[styles.text, this.props.countStyle, extra]}
onLayout={this.onLayout}
>
{text}
</Text>
{unreadCount}
{unreadIndicator}
</View>
</View>
);
@@ -150,12 +163,26 @@ const styles = StyleSheet.create({
top: 2,
},
wrapper: {
alignItems: 'center',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
alignSelf: 'center',
},
text: {
fontSize: 14,
color: 'white',
},
unreadIndicator: {
height: 4,
width: 4,
backgroundColor: '#444',
borderRadius: 4,
},
verticalAlign: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
textAlignVertical: 'center',
},
});

View File

@@ -4,21 +4,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {getChannelFromChannelName} from './channel_link_utils';
import {intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {t} from 'app/utils/i18n';
import {alertErrorWithFallback} from 'app/utils/general';
import {getChannelFromChannelName} from './channel_link_utils';
export default class ChannelLink extends React.PureComponent {
static propTypes = {
channelName: PropTypes.string.isRequired,
channelMentions: PropTypes.object,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
linkStyle: CustomPropTypes.Style,
onChannelLinkPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
channelsByName: PropTypes.object.isRequired,
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
}).isRequired,
};
@@ -30,6 +36,10 @@ export default class ChannelLink extends React.PureComponent {
};
}
static contextTypes = {
intl: intlShape.isRequired,
};
static getDerivedStateFromProps(nextProps, prevState) {
const nextChannel = getChannelFromChannelName(nextProps.channelName, nextProps.channelsByName);
if (nextChannel !== prevState.channel) {
@@ -39,12 +49,35 @@ export default class ChannelLink extends React.PureComponent {
return null;
}
handlePress = () => {
this.props.actions.setChannelDisplayName(this.state.channel.display_name);
this.props.actions.handleSelectChannel(this.state.channel.id);
handlePress = async () => {
let {channel} = this.state;
if (this.props.onChannelLinkPress) {
this.props.onChannelLinkPress(this.state.channel);
if (!channel.id && channel.display_name) {
const {
actions,
channelName,
currentTeamId,
currentUserId,
} = this.props;
const result = await actions.joinChannel(currentUserId, currentTeamId, null, channelName);
if (result.error || !result.data || !result.data.channel) {
const joinFailedMessage = {
id: t('mobile.join_channel.error'),
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again.",
};
alertErrorWithFallback(this.context.intl, result.error || {}, joinFailedMessage, channel.display_name);
} else if (result?.data?.channel) {
channel = result.data.channel;
}
}
if (channel.id) {
this.props.actions.handleSelectChannel(channel.id);
if (this.props.onChannelLinkPress) {
this.props.onChannelLinkPress(channel);
}
}
}
@@ -55,7 +88,10 @@ export default class ChannelLink extends React.PureComponent {
return <Text style={this.props.textStyle}>{`~${this.props.channelName}`}</Text>;
}
const suffix = this.props.channelName.substring(channel.name.length);
let suffix;
if (channel.name) {
suffix = this.props.channelName.substring(channel.name.length);
}
return (
<Text style={this.props.textStyle}>

View File

@@ -5,28 +5,40 @@ import React from 'react';
import {shallow} from 'enzyme';
import {Text} from 'react-native';
import {alertErrorWithFallback} from 'app/utils/general';
import ChannelLink from './channel_link';
jest.mock('react-intl');
jest.mock('app/utils/general', () => ({
alertErrorWithFallback: jest.fn(),
}));
describe('ChannelLink', () => {
const formatMessage = jest.fn();
const channelsByName = {
firstChannel: {id: 'channel_id_1', name: 'firstChannel', display_name: 'First Channel'},
secondChannel: {id: 'channel_id_2', name: 'secondChannel', display_name: 'Second Channel'},
firstChannel: {id: 'channel_id_1', name: 'firstChannel', display_name: 'First Channel', team_id: 'current_team_id'},
secondChannel: {id: 'channel_id_2', name: 'secondChannel', display_name: 'Second Channel', team_id: 'current_team_id'},
};
const baseProps = {
channelName: 'firstChannel',
currentTeamId: 'current_team_id',
currentUserId: 'current_user_id',
linkStyle: {color: '#2389d7'},
onChannelLinkPress: jest.fn(),
textStyle: {color: '#3d3c40', fontSize: 15, lineHeight: 20},
channelsByName,
actions: {
handleSelectChannel: jest.fn(),
setChannelDisplayName: jest.fn(),
joinChannel: jest.fn(),
},
};
test('should match snapshot', () => {
const wrapper = shallow(
<ChannelLink {...baseProps}/>
<ChannelLink {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper.getElement()).toMatchSnapshot();
@@ -59,17 +71,76 @@ describe('ChannelLink', () => {
test('should call props.actions and onChannelLinkPress on handlePress', () => {
const wrapper = shallow(
<ChannelLink {...baseProps}/>
<ChannelLink {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
const channel = channelsByName.firstChannel;
wrapper.setState({channel});
wrapper.instance().handlePress();
expect(baseProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(baseProps.actions.handleSelectChannel).toBeCalledWith(channel.id);
expect(baseProps.actions.setChannelDisplayName).toHaveBeenCalledTimes(1);
expect(baseProps.actions.setChannelDisplayName).toBeCalledWith(channel.display_name);
expect(baseProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
expect(baseProps.onChannelLinkPress).toBeCalledWith(channel);
expect(baseProps.actions.joinChannel).not.toBeCalled();
});
test('should call props.actions.joinChannel on handlePress when user is not member of such channel', async () => {
const newChannelName = 'thirdChannel';
const thirdChannel = {id: 'channel_id_3', name: 'thirdChannel', display_name: 'thirdChannel', team_id: 'current_team_id'};
const error = {message: 'Failed to join a channel'};
const joinChannel = jest.fn().
mockReturnValueOnce({data: {channel: thirdChannel}}).
mockReturnValueOnce({}).
mockReturnValueOnce({data: {}}).
mockReturnValueOnce({error});
const channelMentions = {thirdChannel: {display_name: 'thirdChannel'}};
const newChannelsByName = Object.assign({}, channelMentions, channelsByName);
const newProps = {
...baseProps,
channelsByName: newChannelsByName,
channelName: newChannelName,
actions: {...baseProps.actions, joinChannel},
};
const intl = {formatMessage};
const joinFailedMessage = {
id: 'mobile.join_channel.error',
defaultMessage: 'We couldn\'t join the channel {displayName}. Please check your connection and try again.',
};
const wrapper = shallow(
<ChannelLink {...newProps}/>,
{context: {intl}},
);
await wrapper.instance().handlePress();
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(1);
expect(newProps.actions.joinChannel).toBeCalledWith('current_user_id', 'current_team_id', null, newChannelName);
expect(alertErrorWithFallback).not.toBeCalled();
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(newProps.actions.handleSelectChannel).toHaveBeenLastCalledWith(thirdChannel.id);
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
expect(newProps.onChannelLinkPress).toHaveBeenLastCalledWith(thirdChannel);
// should have called alertErrorWithFallback on error when joining a channel
await wrapper.instance().handlePress();
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(2);
expect(alertErrorWithFallback).toHaveBeenCalledTimes(1);
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, {}, joinFailedMessage, thirdChannel.display_name);
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
await wrapper.instance().handlePress();
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(3);
expect(alertErrorWithFallback).toHaveBeenCalledTimes(2);
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, {}, joinFailedMessage, thirdChannel.display_name);
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
await wrapper.instance().handlePress();
expect(newProps.actions.joinChannel).toHaveBeenCalledTimes(4);
expect(alertErrorWithFallback).toHaveBeenCalledTimes(3);
expect(alertErrorWithFallback).toHaveBeenLastCalledWith(intl, error, joinFailedMessage, thirdChannel.display_name);
expect(newProps.actions.handleSelectChannel).toHaveBeenCalledTimes(1);
expect(newProps.onChannelLinkPress).toHaveBeenCalledTimes(1);
});
});

View File

@@ -3,16 +3,40 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {createSelector} from 'reselect';
import {joinChannel} from 'mattermost-redux/actions/channels';
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/channel';
import {handleSelectChannel} from 'app/actions/views/channel';
import ChannelLink from './channel_link';
function mapStateToProps(state) {
return {
channelsByName: getChannelsNameMapInCurrentTeam(state),
function makeGetChannelNamesMap() {
return createSelector(
getChannelsNameMapInCurrentTeam,
(state, props) => props && props.channelMentions,
(channelsNameMap, channelMentions) => {
if (channelMentions) {
return Object.assign({}, channelMentions, channelsNameMap);
}
return channelsNameMap;
}
);
}
function makeMapStateToProps() {
const getChannelNamesMap = makeGetChannelNamesMap();
return function mapStateToProps(state, ownProps) {
return {
channelsByName: getChannelNamesMap(state, ownProps),
currentTeamId: getCurrentTeamId(state),
currentUserId: getCurrentUserId(state),
};
};
}
@@ -20,9 +44,9 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleSelectChannel,
setChannelDisplayName,
joinChannel,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelLink);
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelLink);

View File

@@ -18,8 +18,6 @@ export default class ChannelLoader extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
}).isRequired,
backgroundColor: PropTypes.string,
@@ -42,7 +40,6 @@ export default class ChannelLoader extends PureComponent {
return {
switch: false,
channel: null,
currentChannelId: null,
};
}
@@ -61,22 +58,13 @@ export default class ChannelLoader extends PureComponent {
if (this.state.switch) {
const {
handleSelectChannel,
markChannelAsRead,
markChannelAsViewed,
setChannelLoading,
} = this.props.actions;
const {channel, currentChannelId} = this.state;
const {channel} = this.state;
setTimeout(() => {
handleSelectChannel(channel.id);
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
setChannelLoading(false);
}, 250);
}
@@ -106,7 +94,7 @@ export default class ChannelLoader extends PureComponent {
if (channel.id === currentChannelId) {
this.props.actions.setChannelLoading(false);
} else {
this.setState({switch: true, channel, currentChannelId});
this.setState({switch: true, channel});
}
};

View File

@@ -4,7 +4,6 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel';
@@ -22,9 +21,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
markChannelAsViewed,
}, dispatch),
};
}

View File

@@ -25,8 +25,6 @@ import {DeviceTypes} from 'app/constants/';
import mattermostBucket from 'app/mattermost_bucket';
import {changeOpacity} from 'app/utils/theme';
import LocalConfig from 'assets/config';
import FileAttachmentIcon from './file_attachment_icon';
const {DOCUMENTS_PATH} = DeviceTypes;
@@ -114,7 +112,7 @@ export default class FileAttachmentDocument extends PureComponent {
this.setState({didCancel: false});
try {
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const certificate = await mattermostBucket.getPreference('cert');
const isDir = await RNFetchBlob.fs.isDir(DOCUMENTS_PATH);
if (!isDir) {
try {
@@ -267,12 +265,18 @@ export default class FileAttachmentDocument extends PureComponent {
}),
}]
);
this.setStatusBarColor();
this.onDonePreviewingFile();
RNFetchBlob.fs.unlink(path);
}
this.setState({downloading: false, progress: 0});
});
// Android does not trigger the event for DoneButtonEvent
// so we'll wait 4 seconds before enabling the tap for open the preview again
if (Platform.OS === 'android') {
setTimeout(this.onDonePreviewingFile, 4000);
}
}
}, delay);
};

View File

@@ -15,7 +15,6 @@ import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retr
import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove';
import mattermostBucket from 'app/mattermost_bucket';
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file';
import LocalConfig from 'assets/config';
export default class FileUploadItem extends PureComponent {
static propTypes = {
@@ -137,7 +136,7 @@ export default class FileUploadItem extends PureComponent {
Client4.trackEvent('api', 'api_files_upload');
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const certificate = await mattermostBucket.getPreference('cert');
const options = {
timeout: 10000,
certificate,

View File

@@ -42,6 +42,7 @@ export default class Markdown extends PureComponent {
autolinkedUrlSchemes: PropTypes.array.isRequired,
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
channelMentions: PropTypes.object,
imageMetadata: PropTypes.object,
isEdited: PropTypes.bool,
isReplyPost: PropTypes.bool,
@@ -222,6 +223,7 @@ export default class Markdown extends PureComponent {
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
onChannelLinkPress={this.props.onChannelLinkPress}
channelName={channelName}
channelMentions={this.props.channelMentions}
/>
);
};

View File

@@ -26,6 +26,8 @@ export default class MarkdownLink extends PureComponent {
static defaultProps = {
onPermalinkPress: () => true,
serverURL: '',
siteURL: '',
};
static contextTypes = {
@@ -40,7 +42,7 @@ export default class MarkdownLink extends PureComponent {
return;
}
const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL);
const match = matchPermalink(url, serverURL) || matchPermalink(url, siteURL) || matchPermalink(url, '');
if (match) {
const teamName = match[1];

View File

@@ -310,11 +310,15 @@ export function getFirstMention(str, mentionKeys) {
let firstMentionIndex = -1;
for (const mention of mentionKeys) {
if (mention.key.trim() === '') {
continue;
}
const flags = mention.caseSensitive ? '' : 'i';
const pattern = new RegExp(`\\b${escapeRegex(mention.key)}_*\\b`, flags);
const match = pattern.exec(str);
if (!match) {
if (!match || match[0] === '') {
continue;
}

View File

@@ -2758,6 +2758,11 @@ describe('Components.Markdown.transform', () => {
input: 'apple banana orange',
mentionKeys: [{key: '*\\3_.'}],
expected: {index: -1, mention: null},
}, {
name: 'no blank mention keys',
input: 'apple banana orange',
mentionKeys: [{key: ''}],
expected: {index: -1, mention: null},
}];
for (const test of tests) {

View File

@@ -4,15 +4,13 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {stopPeriodicStatusUpdates, logout} from 'mattermost-redux/actions/users';
import {stopPeriodicStatusUpdates, startPeriodicStatusUpdates, logout} from 'mattermost-redux/actions/users';
import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {connection} from 'app/actions/device';
import {
initUserStatuses as startPeriodicStatusUpdates,
setCurrentUserStatus,
} from 'app/actions/views/user';
import {markChannelViewedAndRead} from 'app/actions/views/channel';
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
import {getConnection, isLandscape} from 'app/selectors/device';
import NetworkIndicator from './network_indicator';
@@ -37,7 +35,8 @@ function mapDispatchToProps(dispatch) {
connection,
initWebSocket,
logout,
setCurrentUserStatus,
markChannelViewedAndRead,
setCurrentUserStatusOffline,
startPeriodicStatusUpdates,
stopPeriodicStatusUpdates,
}, dispatch),

View File

@@ -22,7 +22,6 @@ import mattermostBucket from 'app/mattermost_bucket';
import PushNotifications from 'app/push_notifications';
import networkConnectionListener, {checkConnection} from 'app/utils/network';
import {t} from 'app/utils/i18n';
import LocalConfig from 'assets/config';
import {RequestStatus} from 'mattermost-redux/constants';
@@ -44,8 +43,9 @@ export default class NetworkIndicator extends PureComponent {
closeWebSocket: PropTypes.func.isRequired,
connection: PropTypes.func.isRequired,
initWebSocket: PropTypes.func.isRequired,
markChannelViewedAndRead: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
setCurrentUserStatus: PropTypes.func.isRequired,
setCurrentUserStatusOffline: PropTypes.func.isRequired,
startPeriodicStatusUpdates: PropTypes.func.isRequired,
stopPeriodicStatusUpdates: PropTypes.func.isRequired,
}).isRequired,
@@ -67,9 +67,12 @@ export default class NetworkIndicator extends PureComponent {
constructor(props) {
super(props);
this.state = {
opacity: 0,
};
const navBar = this.getNavBarHeight(props.isLandscape);
this.top = new Animated.Value(navBar - HEIGHT);
this.opacity = 0;
this.clearNotificationTimeout = null;
this.backgroundColor = new Animated.Value(0);
@@ -162,7 +165,6 @@ export default class NetworkIndicator extends PureComponent {
};
connected = () => {
this.props.actions.setCurrentUserStatus(true);
Animated.sequence([
Animated.timing(
this.backgroundColor, {
@@ -179,7 +181,9 @@ export default class NetworkIndicator extends PureComponent {
),
]).start(() => {
this.backgroundColor.setValue(0);
this.opacity = 0;
this.setState({
opacity: 0,
});
});
};
@@ -223,7 +227,7 @@ export default class NetworkIndicator extends PureComponent {
};
handleAppStateChange = async (appState) => {
const {currentChannelId} = this.props;
const {actions, currentChannelId} = this.props;
const active = appState === 'active';
if (active) {
@@ -235,6 +239,7 @@ export default class NetworkIndicator extends PureComponent {
// foreground by tapping a notification from another channel
this.clearNotificationTimeout = setTimeout(() => {
PushNotifications.clearChannelNotifications(currentChannelId);
actions.markChannelViewedAndRead(currentChannelId);
}, 1000);
}
} else {
@@ -251,7 +256,13 @@ export default class NetworkIndicator extends PureComponent {
this.initializeWebSocket();
startPeriodicStatusUpdates();
this.firstRun = false;
return;
// if the state of the internet connection was previously known to be false,
// don't exit connection handler in order for application to register it has
// reconnected to the internet
if (this.hasInternet !== false) {
return;
}
}
// Prevent for being called more than once.
@@ -283,7 +294,7 @@ export default class NetworkIndicator extends PureComponent {
const platform = Platform.OS;
let certificate = null;
if (platform === 'ios') {
certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
certificate = await mattermostBucket.getPreference('cert');
}
initWebSocket(platform, null, null, null, {certificate, forceConnection: true}).catch(() => {
@@ -316,7 +327,9 @@ export default class NetworkIndicator extends PureComponent {
};
show = () => {
this.opacity = 1;
this.setState({
opacity: 1,
});
Animated.timing(
this.top, {
@@ -324,7 +337,7 @@ export default class NetworkIndicator extends PureComponent {
duration: 300,
}
).start(() => {
this.props.actions.setCurrentUserStatus(false);
this.props.actions.setCurrentUserStatusOffline();
});
};
@@ -376,7 +389,7 @@ export default class NetworkIndicator extends PureComponent {
}
return (
<Animated.View style={[styles.container, {top: this.top, backgroundColor: background, opacity: this.opacity}]}>
<Animated.View style={[styles.container, {top: this.top, backgroundColor: background, opacity: this.state.opacity}]}>
<Animated.View style={styles.wrapper}>
<FormattedText
defaultMessage={defaultMessage}

View File

@@ -23,8 +23,8 @@ function isConsecutivePost(state, ownProps) {
let consecutivePost = false;
if (previousPost) {
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
const prevPostFromWebhook = Boolean(previousPost.props && previousPost.props.from_webhook);
const postFromWebhook = Boolean(post?.props?.from_webhook); // eslint-disable-line camelcase
const prevPostFromWebhook = Boolean(previousPost?.props?.from_webhook); // eslint-disable-line camelcase
if (previousPost && previousPost.user_id === post.user_id &&
post.create_at - previousPost.create_at <= Posts.POST_COLLAPSE_TIMEOUT &&
!postFromWebhook && !prevPostFromWebhook &&

View File

@@ -236,7 +236,21 @@ export default class PostBody extends PureComponent {
}
renderPostAdditionalContent = (blockStyles, messageStyle, textStyles) => {
const {isReplyPost, message, navigator, onHashtagPress, onPermalinkPress, postId, postProps, metadata} = this.props;
const {
isReplyPost,
isSystemMessage,
message,
metadata,
navigator,
onHashtagPress,
onPermalinkPress,
postId,
postProps,
} = this.props;
if (isSystemMessage) {
return null;
}
if (metadata && !metadata.embeds) {
return null;
@@ -365,6 +379,7 @@ export default class PostBody extends PureComponent {
<Markdown
baseTextStyle={messageStyle}
blockStyles={blockStyles}
channelMentions={postProps.channel_mentions}
imageMetadata={metadata?.images}
isEdited={hasBeenEdited}
isReplyPost={isReplyPost}

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {Preferences} from 'mattermost-redux/constants';
import PostBodyAdditionalContent from 'app/components/post_body_additional_content';
import {shallowWithIntl} from 'test/intl-test-helper';
import PostBody from './post_body.js';
describe('PostBody', () => {
const baseProps = {
canDelete: true,
channelIsReadOnly: false,
deviceHeight: 1920,
fileIds: [],
hasBeenDeleted: false,
hasBeenEdited: false,
hasReactions: false,
highlight: false,
isFailed: false,
isFlagged: false,
isPending: false,
isPostAddChannelMember: false,
isPostEphemeral: false,
isReplyPost: false,
isSearchResult: false,
isSystemMessage: false,
managedConfig: {},
message: 'Hello, World!',
navigator: {},
onFailedPostPress: jest.fn(),
onHashtagPress: jest.fn(),
onPermalinkPress: jest.fn(),
onPress: jest.fn(),
postId: 'post',
postProps: {},
postType: '',
replyBarStyle: [],
showAddReaction: true,
showLongPost: true,
isEmojiOnly: false,
shouldRenderJumboEmoji: false,
theme: Preferences.THEMES.default,
};
test('should mount additional content for non-system messages', () => {
const props = {
...baseProps,
isSystemMessage: false,
};
const wrapper = shallowWithIntl(<PostBody {...props}/>);
expect(wrapper.find(PostBodyAdditionalContent).exists()).toBeTruthy();
});
test('should not mount additional content for system messages', () => {
const props = {
...baseProps,
isSystemMessage: true,
};
const wrapper = shallowWithIntl(<PostBody {...props}/>);
expect(wrapper.find(PostBodyAdditionalContent).exists()).toBeFalsy();
});
});

View File

@@ -184,7 +184,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
return attachments;
}
if (!openGraphData) {
if (!openGraphData && metadata) {
return null;
}

View File

@@ -36,12 +36,12 @@ function makeMapStateToProps() {
createAt: post.create_at,
displayName: displayUsername(user, teammateNameDisplay),
enablePostUsernameOverride: config.EnablePostUsernameOverride === 'true',
fromWebHook: post.props && post.props.from_webhook === 'true',
fromWebHook: post?.props?.from_webhook === 'true', // eslint-disable-line camelcase
militaryTime,
isPendingOrFailedPost: isPostPendingOrFailed(post),
isSystemMessage: isSystemMessage(post),
fromAutoResponder: fromAutoResponder(post),
overrideUsername: post.props && post.props.override_username,
overrideUsername: post?.props?.override_username, // eslint-disable-line camelcase
theme: getTheme(state),
username: user.username,
userTimezone,

View File

@@ -4,7 +4,10 @@
import React from 'react';
import {FlatList, StyleSheet} from 'react-native';
import {debounce} from 'mattermost-redux/actions/helpers';
import {ListTypes} from 'app/constants';
import {THREAD} from 'app/constants/screen';
import {makeExtraData} from 'app/utils/list_view';
import PostListBase from './post_list_base';
@@ -59,8 +62,8 @@ export default class PostList extends PostListBase {
handleScroll = (event) => {
const pageOffsetY = event.nativeEvent.contentOffset.y;
const contentHeight = event.nativeEvent.contentSize.height;
if (pageOffsetY > 0) {
const contentHeight = event.nativeEvent.contentSize.height;
const direction = (this.contentOffsetY < pageOffsetY) ?
ListTypes.VISIBILITY_SCROLL_UP :
ListTypes.VISIBILITY_SCROLL_DOWN;
@@ -72,9 +75,26 @@ export default class PostList extends PostListBase {
) {
this.props.onLoadMoreUp();
}
} else if (pageOffsetY < 0) {
if (this.state.postListHeight > contentHeight || this.props.location === THREAD) {
// Posting a message like multiline or jumbo emojis causes the FlatList component for iOS
// to render RefreshControl component and remain the space as is when it's unmounted,
// leaving a whitespace of ~64 units of height between input box and post list.
// This condition explicitly pull down the list to recent post when pageOffsetY is less than zero,
// and the height of the layout is greater than its content or is on a thread screen.
this.handleScrollToRecentPost();
}
}
};
handleScrollToRecentPost = debounce(() => {
this.refs.list.scrollToIndex({
animated: true,
index: 0,
viewPosition: 1,
});
}, 100);
handleScrollToIndexFailed = () => {
requestAnimationFrame(() => {
this.hasDoneInitialScroll = false;

View File

@@ -44,12 +44,15 @@ export default class PostListBase extends PureComponent {
shouldRenderReplyButton: PropTypes.bool,
siteURL: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
location: PropTypes.string,
};
static defaultProps = {
onLoadMoreUp: () => true,
renderFooter: () => null,
refreshing: false,
serverURL: '',
siteURL: '',
};
componentWillMount() {

View File

@@ -18,11 +18,11 @@ function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId);
return {
enablePostIconOverride: config.EnablePostIconOverride === 'true',
fromWebHook: post.props && post.props.from_webhook === 'true',
enablePostIconOverride: config.EnablePostIconOverride === 'true' && post?.props?.use_user_icon !== 'true', // eslint-disable-line camelcase
fromWebHook: post?.props?.from_webhook === 'true', // eslint-disable-line camelcase
isSystemMessage: isSystemMessage(post),
fromAutoResponder: fromAutoResponder(post),
overrideIconUrl: post.props && post.props.override_icon_url,
overrideIconUrl: post?.props?.override_icon_url, // eslint-disable-line camelcase
userId: post.user_id,
theme: getTheme(state),
};

View File

@@ -48,7 +48,6 @@ export default class ProfilePicture extends PureComponent {
state = {
pictureUrl: null,
otherImageProps: {},
};
componentDidMount() {
@@ -99,34 +98,21 @@ export default class ProfilePicture extends PureComponent {
}
};
showDefaultImage = () => {
if (this.mounted) {
this.setState({otherImageProps: {defaultSource: placeholder}});
}
};
clearProfileImageUri = () => {
if (this.props.isCurrentUser && this.props.profileImageUri !== '') {
this.props.actions.setProfileImageUri('');
}
}
componentDidUpdate(prevProps, prevState) {
if (!this.props.edit) {
if (this.state.otherImageProps !== prevState.otherImageProps) {
this.showDefaultImage();
}
}
componentDidUpdate(prevProps) {
if (this.props.profileImageRemove !== prevProps.profileImageRemove) {
this.setImageURL(null);
this.showDefaultImage();
}
}
render() {
const {edit, showStatus, theme} = this.props;
const {pictureUrl, otherImageProps} = this.state;
const {pictureUrl} = this.state;
const style = getStyleSheet(theme);
let statusIcon;
@@ -155,6 +141,7 @@ export default class ProfilePicture extends PureComponent {
}
let source = null;
let image;
if (pictureUrl) {
let prefix = '';
if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') &&
@@ -165,16 +152,26 @@ export default class ProfilePicture extends PureComponent {
source = {
uri: `${prefix}${pictureUrl}`,
};
}
return (
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
image = (
<Image
key={pictureUrl}
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
source={source}
{...otherImageProps}
/>
);
} else {
image = (
<Image
style={{width: this.props.size, height: this.props.size, borderRadius: this.props.size / 2}}
source={placeholder}
/>
);
}
return (
<View style={{width: this.props.size + STATUS_BUFFER, height: this.props.size + STATUS_BUFFER}}>
{image}
{(showStatus || edit) &&
<View style={[style.statusWrapper, statusStyle, {borderRadius: this.props.statusSize / 2}]}>
{statusIcon}

View File

@@ -15,6 +15,10 @@ export default class ProfilePictureButton extends PureComponent {
currentUser: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
removeProfileImage: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
constructor(props) {
@@ -55,7 +59,7 @@ export default class ProfilePictureButton extends PureComponent {
};
}
return null;
}
};
render() {
const {children, ...props} = this.props;

View File

@@ -18,6 +18,9 @@ export default class ProgressiveImage extends PureComponent {
defaultSource: PropTypes.oneOfType([PropTypes.object, PropTypes.number]), // this should be provided by the component
filename: PropTypes.string,
imageUri: PropTypes.string,
onError: PropTypes.func,
resizeMethod: PropTypes.string,
resizeMode: PropTypes.string,
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
thumbnailUri: PropTypes.string,

View File

@@ -44,7 +44,7 @@ export default class Reactions extends PureComponent {
componentDidMount() {
const {actions, postId, reactions} = this.props;
if (reactions) {
if (!reactions) {
actions.getReactionsForPost(postId);
}
}

View File

@@ -9,6 +9,7 @@ import {
Text,
TouchableHighlight,
View,
Platform,
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
@@ -396,7 +397,8 @@ class FilteredList extends Component {
renderItem={this.renderItem}
keyExtractor={(item) => item.id}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
keyboardDismissMode={Platform.OS === 'ios' ? 'interactive' : 'on-drag'}
keyboardShouldPersistTaps='always'
maxToRenderPerBatch={10}
viewabilityConfig={VIEWABILITY_CONFIG}
/>

View File

@@ -19,6 +19,7 @@ import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item';
import {ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import {t} from 'app/utils/i18n';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
@@ -88,48 +89,48 @@ export default class List extends PureComponent {
switch (sectionType) {
case SidebarSectionTypes.UNREADS:
return {
id: 'mobile.channel_list.unreads',
id: t('mobile.channel_list.unreads'),
defaultMessage: 'UNREADS',
};
case SidebarSectionTypes.FAVORITE:
return {
id: 'sidebar.favorite',
id: t('sidebar.favorite'),
defaultMessage: 'FAVORITES',
};
case SidebarSectionTypes.PUBLIC:
return {
action: this.goToMoreChannels,
id: 'sidebar.channels',
id: t('sidebar.channels'),
defaultMessage: 'PUBLIC CHANNELS',
};
case SidebarSectionTypes.PRIVATE:
return {
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: 'sidebar.pg',
id: t('sidebar.pg'),
defaultMessage: 'PRIVATE CHANNELS',
};
case SidebarSectionTypes.DIRECT:
return {
action: this.goToDirectMessages,
id: 'sidebar.direct',
id: t('sidebar.direct'),
defaultMessage: 'DIRECT MESSAGES',
};
case SidebarSectionTypes.RECENT_ACTIVITY:
return {
action: this.showCreateChannelOptions,
id: 'sidebar.types.recent',
id: t('sidebar.types.recent'),
defaultMessage: 'RECENT ACTIVITY',
};
case SidebarSectionTypes.ALPHA:
return {
action: this.showCreateChannelOptions,
id: 'mobile.channel_list.channels',
id: t('mobile.channel_list.channels'),
defaultMessage: 'CHANNELS',
};
default:
return {
action: this.showCreateChannelOptions,
id: 'mobile.channel_list.channels',
id: t('mobile.channel_list.channels'),
defaultMessage: 'CHANNELS',
};
}

View File

@@ -164,14 +164,7 @@ export default class ChannelSidebar extends Component {
};
selectChannel = (channel, currentChannelId, closeDrawer = true) => {
const {
actions,
} = this.props;
const {
setChannelLoading,
setChannelDisplayName,
} = actions;
const {setChannelLoading} = this.props.actions;
tracker.channelSwitch = Date.now();
@@ -195,7 +188,6 @@ export default class ChannelSidebar extends Component {
return;
}
setChannelDisplayName(channel.display_name);
EventEmitter.emit('switch_channel', channel, currentChannelId);
};

4
app/constants/screen.js Normal file
View File

@@ -0,0 +1,4 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const THREAD = 'thread';

View File

@@ -87,6 +87,7 @@ const ViewTypes = keyMirror({
SELECTED_ACTION_MENU: null,
SUBMIT_ATTACHMENT_MENU_ACTION: null,
SELECT_CHANNEL_WITH_MEMBER: null,
});
export default {

View File

@@ -9,6 +9,7 @@ import {Client4} from 'mattermost-redux/client';
import {ClientError} from 'mattermost-redux/client/client4';
import mattermostBucket from 'app/mattermost_bucket';
import mattermostManaged from 'app/mattermost_managed';
import LocalConfig from 'assets/config';
import {t} from 'app/utils/i18n';
@@ -18,6 +19,12 @@ import {t} from 'app/utils/i18n';
const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
const HEADER_TOKEN = 'Token';
let managedConfig;
mattermostManaged.addEventListener('fetch_managed_config', (config) => {
managedConfig = config;
});
const handleRedirectProtocol = (url, response) => {
const serverUrl = Client4.getUrl();
const parsed = urlParse(url);
@@ -30,15 +37,33 @@ const handleRedirectProtocol = (url, response) => {
};
Client4.doFetchWithResponse = async (url, options) => {
if (!Client4.online) {
throw new ClientError(Client4.getUrl(), {
message: 'no internet connection',
url,
});
}
// Removing the check of this flag to be handled natively.
// In case Android presents the out of memory issue, consider uncommenting line 42-47.
// if (!Client4.online) {
// throw new ClientError(Client4.getUrl(), {
// message: 'no internet connection',
// url,
// });
// }
const customHeaders = LocalConfig.CustomRequestHeaders;
let requestOptions = Client4.getOptions(options);
let waitsForConnectivity = false;
let timeoutIntervalForResource = 30;
if (managedConfig?.useVPN === 'true') {
waitsForConnectivity = true;
}
if (managedConfig?.timeoutVPN) {
timeoutIntervalForResource = parseInt(managedConfig.timeoutVPN, 10);
}
let requestOptions = {
...Client4.getOptions(options),
waitsForConnectivity,
timeoutIntervalForResource,
};
if (customHeaders && Object.keys(customHeaders).length > 0) {
requestOptions = {
...requestOptions,
@@ -121,8 +146,15 @@ Client4.doFetchWithResponse = async (url, options) => {
const initFetchConfig = async () => {
let fetchConfig = {};
try {
managedConfig = await mattermostManaged.getConfig();
} catch {
// no managed config
}
if (Platform.OS === 'ios') {
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const certificate = await mattermostBucket.getPreference('cert');
fetchConfig = {
auto: true,
certificate,

View File

@@ -123,10 +123,6 @@ const state = {
},
},
users: {
checkMfa: {
status: 'not_started',
error: null,
},
login: {
status: 'not_started',
error: null,

View File

@@ -24,12 +24,15 @@ import {setAppState, setServerVersion} from 'mattermost-redux/actions/general';
import {loadMe, logout} from 'mattermost-redux/actions/users';
import {close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {General} from 'mattermost-redux/constants';
import {handleLoginIdChanged} from 'app/actions/views/login';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {selectDefaultChannel} from 'app/actions/views/channel';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device';
import {handleLoginIdChanged} from 'app/actions/views/login';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import {loadConfigAndLicense, startDataCleanup} from 'app/actions/views/root';
import initialState from 'app/initial_state';
import configureStore from 'app/store';
import {NavigationTypes} from 'app/constants';
@@ -38,14 +41,6 @@ import mattermostManaged from 'app/mattermost_managed';
import {configurePushNotifications} from 'app/utils/push_notifications';
import PushNotifications from 'app/push_notifications';
import {registerScreens} from 'app/screens';
import {
setDeviceDimensions,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight,
} from 'app/actions/device';
import {loadConfigAndLicense, startDataCleanup} from 'app/actions/views/root';
import {setChannelDisplayName} from 'app/actions/views/channel';
import {deleteFileCache} from 'app/utils/file';
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
import {t} from 'app/utils/i18n';
@@ -96,7 +91,7 @@ const initializeModules = () => {
EventEmitter.on(NavigationTypes.RESTART_APP, restartApp);
EventEmitter.on(General.SERVER_VERSION_CHANGED, handleServerVersionChanged);
EventEmitter.on(General.CONFIG_CHANGED, handleConfigChanged);
EventEmitter.on(General.DEFAULT_CHANNEL, handleResetChannelDisplayName);
EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, handleSwithToDefaultChannel);
Dimensions.addEventListener('change', handleOrientationChange);
mattermostManaged.addEventListener('managedConfigDidChange', () => {
handleManagedConfig(true);
@@ -326,7 +321,7 @@ const handleAuthentication = async (vendor) => {
const translations = app.getTranslations();
if (isSecured) {
try {
mattermostBucket.setPreference('emm', vendor, LocalConfig.AppGroupId);
mattermostBucket.setPreference('emm', vendor);
await mattermostManaged.authenticate({
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', vendor),
fallbackToPasscode: true,
@@ -342,8 +337,8 @@ const handleAuthentication = async (vendor) => {
return true;
};
const handleResetChannelDisplayName = (displayName) => {
store.dispatch(setChannelDisplayName(displayName));
const handleSwithToDefaultChannel = (teamId) => {
store.dispatch(selectDefaultChannel(teamId));
};
const launchSelectServer = () => {

View File

@@ -4,17 +4,17 @@
import {NativeModules, Platform} from 'react-native';
// TODO: Remove platform specific once android is implemented
const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucket : null;
const MattermostBucket = Platform.OS === 'ios' ? NativeModules.MattermostBucketModule : null;
export default {
setPreference: (key, value, groupName) => {
setPreference: (key, value) => {
if (MattermostBucket) {
MattermostBucket.setPreference(key, value, groupName);
MattermostBucket.setPreference(key, value);
}
},
getPreference: async (key, groupName) => {
getPreference: async (key) => {
if (MattermostBucket) {
const value = await MattermostBucket.getPreference(key, groupName);
const value = await MattermostBucket.getPreference(key);
if (value) {
try {
return JSON.parse(value);
@@ -26,19 +26,19 @@ export default {
return null;
},
removePreference: (key, groupName) => {
removePreference: (key) => {
if (MattermostBucket) {
MattermostBucket.removePreference(key, groupName);
MattermostBucket.removePreference(key);
}
},
writeToFile: (fileName, content, groupName) => {
writeToFile: (fileName, content) => {
if (MattermostBucket) {
MattermostBucket.writeToFile(fileName, content, groupName);
MattermostBucket.writeToFile(fileName, content);
}
},
readFromFile: async (fileName, groupName) => {
readFromFile: async (fileName) => {
if (MattermostBucket) {
const value = await MattermostBucket.readFromFile(fileName, groupName);
const value = await MattermostBucket.readFromFile(fileName);
if (value) {
try {
return JSON.parse(value);
@@ -50,9 +50,9 @@ export default {
return null;
},
removeFile: (fileName, groupName) => {
removeFile: (fileName) => {
if (MattermostBucket) {
MattermostBucket.removeFile(fileName, groupName);
MattermostBucket.removeFile(fileName);
}
},
};

View File

@@ -152,6 +152,8 @@ class PushNotification {
clearChannelNotifications(channelId) {
NotificationsIOS.getDeliveredNotifications((notifications) => {
const ids = [];
let badgeCount = notifications.length;
for (let i = 0; i < notifications.length; i++) {
const notification = notifications[i];
@@ -161,8 +163,11 @@ class PushNotification {
}
if (ids.length) {
badgeCount -= ids.length;
NotificationsIOS.removeDeliveredNotifications(ids);
}
this.setApplicationIconBadgeNumber(badgeCount);
});
}
}

View File

@@ -332,6 +332,22 @@ function loadMorePostsVisible(state = true, action) {
}
}
function lastChannelViewTime(state = {}, action) {
switch (action.type) {
case ViewTypes.SELECT_CHANNEL_WITH_MEMBER: {
if (action.member) {
const nextState = {...state};
nextState[action.data] = action.member.last_viewed_at;
return nextState;
}
return state;
}
default:
return state;
}
}
export default combineReducers({
displayName,
drafts,
@@ -343,4 +359,5 @@ export default combineReducers({
lastGetPosts,
retryFailed,
loadMorePostsVisible,
lastChannelViewTime,
});

View File

@@ -14,6 +14,8 @@ import {
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {app} from 'app/mattermost';
import InteractiveDialogController from 'app/components/interactive_dialog_controller';
import EmptyToolbar from 'app/components/start/empty_toolbar';
import ChannelLoader from 'app/components/channel_loader';
@@ -86,6 +88,10 @@ export default class Channel extends PureComponent {
} else {
this.props.actions.selectDefaultTeam();
}
if (this.props.currentChannelId) {
PushNotifications.clearChannelNotifications(this.props.currentChannelId);
}
}
componentDidMount() {
@@ -243,9 +249,12 @@ export default class Channel extends PureComponent {
loadChannelsIfNecessary(teamId).then(() => {
loadProfilesAndTeamMembersForDMSidebar(teamId);
selectInitialChannel(teamId);
}).catch(() => {
selectInitialChannel(teamId);
if (app.startAppFromPushNotification) {
app.setStartAppFromPushNotification(false);
} else {
selectInitialChannel(teamId);
}
});
};

View File

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {
PanResponder,
Platform,
TouchableOpacity,
View,
} from 'react-native';
@@ -14,7 +13,6 @@ import {
import Icon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
import PushNotifications from 'app/push_notifications';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {preventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -62,10 +60,6 @@ class ChannelDrawerButton extends PureComponent {
EventEmitter.on('drawer_opacity', this.setOpacity);
}
componentDidUpdate() {
PushNotifications.setApplicationIconBadgeNumber(this.props.mentionCount);
}
componentWillUnmount() {
EventEmitter.off('drawer_opacity', this.setOpacity);
}
@@ -131,8 +125,10 @@ class ChannelDrawerButton extends PureComponent {
style={style.container}
>
<View style={[style.wrapper, {opacity: this.state.opacity}]}>
{icon}
{badge}
<View>
{icon}
{badge}
</View>
</View>
</TouchableOpacity>
);
@@ -156,19 +152,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
left: 3,
left: -13,
padding: 3,
position: 'absolute',
right: 0,
...Platform.select({
android: {
top: 10,
},
ios: {
top: 5,
},
}),
top: -4,
},
mention: {
color: theme.mentionColor,

View File

@@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {selectPost} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentChannelId, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
@@ -18,7 +18,6 @@ import ChannelPostList from './channel_post_list';
function mapStateToProps(state) {
const channelId = getCurrentChannelId(state);
const channelRefreshingFailed = state.views.channel.retryFailed;
const currentChannelMember = getMyCurrentChannelMembership(state);
return {
channelId,
@@ -27,7 +26,7 @@ function mapStateToProps(state) {
deviceHeight: state.device.dimension.deviceHeight,
postIds: getPostIdsInCurrentChannel(state),
postVisibility: state.views.channel.postVisibility[channelId],
lastViewedAt: currentChannelMember && currentChannelMember.last_viewed_at,
lastViewedAt: state.views.channel.lastChannelViewTime[channelId],
loadMorePostsVisible: state.views.channel.loadMorePostsVisible,
refreshing: state.views.channel.refreshing,
theme: getTheme(state),

View File

@@ -12,7 +12,6 @@ import {
} from 'react-native';
import {General, Users} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import StatusBar from 'app/components/status_bar';
import {preventDoubleTap} from 'app/utils/tap';
@@ -42,6 +41,7 @@ export default class ChannelInfo extends PureComponent {
updateChannelNotifyProps: PropTypes.func.isRequired,
selectPenultimateChannel: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}),
viewArchivedChannels: PropTypes.bool.isRequired,
canDeleteChannel: PropTypes.bool.isRequired,
@@ -104,7 +104,7 @@ export default class ChannelInfo extends PureComponent {
close = (redirect = true) => {
if (redirect) {
EventEmitter.emit(General.DEFAULT_CHANNEL, '');
this.props.actions.setChannelDisplayName('');
}
if (Platform.OS === 'android') {
this.props.navigator.dismissModal({animated: true});

View File

@@ -33,10 +33,11 @@ import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general
import {
closeDMChannel,
closeGMChannel,
handleSelectChannel,
leaveChannel,
loadChannelsByTeamName,
selectPenultimateChannel,
handleSelectChannel,
setChannelDisplayName,
} from 'app/actions/views/channel';
import ChannelInfo from './channel_info';
@@ -107,6 +108,7 @@ function mapDispatchToProps(dispatch) {
selectFocusedPostId,
updateChannelNotifyProps,
selectPenultimateChannel,
setChannelDisplayName,
handleSelectChannel,
}, dispatch),
};

View File

@@ -12,7 +12,7 @@ export default class ChannelPeek extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadPostsIfNecessaryWithRetry: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
markChannelViewedAndRead: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string.isRequired,
currentUserId: PropTypes.string,
@@ -59,7 +59,7 @@ export default class ChannelPeek extends PureComponent {
if (event.type === 'PreviewActionPress') {
if (event.id === 'action-mark-as-read') {
const {actions, channelId} = this.props;
actions.markChannelAsRead(channelId);
actions.markChannelViewedAndRead(channelId);
}
}
};

View File

@@ -4,11 +4,10 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {loadPostsIfNecessaryWithRetry} from 'app/actions/views/channel';
import {loadPostsIfNecessaryWithRetry, markChannelViewedAndRead} from 'app/actions/views/channel';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelPeek from './channel_peek';
@@ -30,7 +29,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
loadPostsIfNecessaryWithRetry,
markChannelAsRead,
markChannelViewedAndRead,
}, dispatch),
};
}

View File

@@ -53,7 +53,7 @@ exports[`edit_profile should match snapshot 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -66,7 +66,7 @@ exports[`edit_profile should match snapshot 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],

View File

@@ -24,7 +24,7 @@ import StatusBar from 'app/components/status_bar/index';
import ProfilePictureButton from 'app/components/profile_picture_button';
import ProfilePicture from 'app/components/profile_picture';
import mattermostBucket from 'app/mattermost_bucket';
import LocalConfig from 'assets/config';
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
const MAX_SIZE = 20 * 1024 * 1024;
@@ -91,8 +91,8 @@ export default class EditProfile extends PureComponent {
rightButtons: [this.rightButton],
};
this.leftButton.title = context.intl.formatMessage({id: 'mobile.account.settings.cancel', defaultMessage: 'Cancel'});
this.rightButton.title = context.intl.formatMessage({id: 'mobile.account.settings.save', defaultMessage: 'Save'});
this.leftButton.title = context.intl.formatMessage({id: t('mobile.account.settings.cancel'), defaultMessage: 'Cancel'});
this.rightButton.title = context.intl.formatMessage({id: t('mobile.account.settings.save'), defaultMessage: 'Save'});
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons(buttons);
@@ -235,7 +235,7 @@ export default class EditProfile extends PureComponent {
type: fileData.type,
};
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const certificate = await mattermostBucket.getPreference('cert');
const options = {
timeout: 10000,
certificate,
@@ -354,21 +354,17 @@ export default class EditProfile extends PureComponent {
renderEmailSettings = () => {
const {formatMessage} = this.context.intl;
const {config, currentUser, theme} = this.props;
const {currentUser, theme} = this.props;
const {email} = this.state;
let helpText;
let disabled = false;
if (config.SendEmailNotifications !== 'true') {
disabled = true;
if (currentUser.auth_service === '') {
helpText = formatMessage({
id: 'user.settings.general.emailHelp1',
defaultMessage: 'Email is used for sign-in, notifications, and password reset. Email requires verification if changed.',
id: 'user.settings.general.emailCantUpdate',
defaultMessage: 'Email must be updated using a web client or desktop application.',
});
} else if (currentUser.auth_service !== '') {
disabled = true;
} else {
switch (currentUser.auth_service) {
case 'gitlab':
helpText = formatMessage({
@@ -406,7 +402,7 @@ export default class EditProfile extends PureComponent {
return (
<View>
<TextSetting
disabled={disabled}
disabled={true}
id='email'
label={holders.email}
disabledText={helpText}

View File

@@ -19,7 +19,6 @@ import {
app,
store,
} from 'app/mattermost';
import {loadFromPushNotification} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import PushNotifications from 'app/push_notifications';
import {stripTrailingSlashes} from 'app/utils/url';
@@ -202,11 +201,8 @@ export default class Entry extends PureComponent {
const {data, text, badge, completed} = notificationData;
// if the notification has a completed property it means that we are replying to a notification
// and in case it doesn't it means we just opened the notification
if (completed) {
onPushNotificationReply(data, text, badge, completed);
} else {
await store.dispatch(loadFromPushNotification(notification));
}
PushNotifications.resetNotification();
}

View File

@@ -16,8 +16,6 @@ import mattermostBucket from 'app/mattermost_bucket';
import {getLocalFilePathFromFile} from 'app/utils/file';
import {emptyFunction} from 'app/utils/general';
import LocalConfig from 'assets/config';
import DownloaderBottomContent from './downloader_bottom_content.js';
const {View: AnimatedView} = Animated;
@@ -236,7 +234,7 @@ export default class Downloader extends PureComponent {
this.setState({didCancel: false});
}
const certificate = await mattermostBucket.getPreference('cert', LocalConfig.AppGroupId);
const certificate = await mattermostBucket.getPreference('cert');
const imageUrl = Client4.getFileUrl(data.id);
const options = {
session: data.id,

View File

@@ -8,17 +8,16 @@ import LoginActions from 'app/actions/views/login';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {checkMfa, login} from 'mattermost-redux/actions/users';
import {login} from 'mattermost-redux/actions/users';
import Login from './login.js';
function mapStateToProps(state) {
const {checkMfa: checkMfaRequest, login: loginRequest} = state.requests.users;
const {login: loginRequest} = state.requests.users;
const config = getConfig(state);
const license = getLicense(state);
return {
...state.views.login,
checkMfaRequest,
loginRequest,
config,
license,
@@ -30,7 +29,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...LoginActions,
checkMfa,
login,
}, dispatch),
};

View File

@@ -22,14 +22,16 @@ import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
import PushNotifications from 'app/push_notifications';
import {GlobalStyles} from 'app/styles';
import {preventDoubleTap} from 'app/utils/tap';
import tracker from 'app/utils/time_tracker';
import {t} from 'app/utils/i18n';
import {setMfaPreflightDone, getMfaPreflightDone} from 'app/utils/security';
import {RequestStatus} from 'mattermost-redux/constants';
const mfaExpectedErrors = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error'];
export default class Login extends PureComponent {
static propTypes = {
navigator: PropTypes.object,
@@ -38,15 +40,13 @@ export default class Login extends PureComponent {
handleLoginIdChanged: PropTypes.func.isRequired,
handlePasswordChanged: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
getSession: PropTypes.func.isRequired,
checkMfa: PropTypes.func.isRequired,
scheduleExpiredNotification: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
}).isRequired,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired,
loginId: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
checkMfaRequest: PropTypes.object.isRequired,
loginRequest: PropTypes.object.isRequired,
};
@@ -62,13 +62,14 @@ export default class Login extends PureComponent {
};
}
componentWillMount() {
componentDidMount() {
Dimensions.addEventListener('change', this.orientationDidChange);
setMfaPreflightDone(false);
}
componentWillReceiveProps(nextProps) {
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
this.props.actions.handleSuccessfulLogin().then(this.props.actions.getSession).then(this.goToChannel);
this.props.actions.handleSuccessfulLogin().then(this.goToChannel);
} else if (this.props.loginRequest.status !== nextProps.loginRequest.status && nextProps.loginRequest.status !== RequestStatus.STARTED) {
this.setState({isLoading: false});
}
@@ -78,23 +79,11 @@ export default class Login extends PureComponent {
Dimensions.removeEventListener('change', this.orientationDidChange);
}
goToChannel = (expiresAt) => {
const {intl} = this.context;
goToChannel = () => {
const {navigator} = this.props;
tracker.initialLoad = Date.now();
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message: intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
}),
userInfo: {
localNotification: true,
},
});
}
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
@@ -195,23 +184,27 @@ export default class Login extends PureComponent {
return;
}
if (this.props.config.EnableMultifactorAuthentication === 'true') {
const result = await this.props.actions.checkMfa(this.props.loginId);
if (result.data) {
this.goToMfa();
} else {
this.signIn();
}
} else {
this.signIn();
}
this.signIn();
});
});
scheduleSessionExpiredNotification = () => {
const {intl} = this.context;
const {actions} = this.props;
actions.scheduleExpiredNotification(intl);
};
signIn = () => {
const {actions, loginId, loginRequest, password} = this.props;
if (loginRequest.status !== RequestStatus.STARTED) {
actions.login(loginId.toLowerCase(), password);
actions.login(loginId.toLowerCase(), password).then(this.checkLoginResponse);
}
};
checkLoginResponse = (data) => {
if (mfaExpectedErrors.includes(data?.error?.server_error_id)) { // eslint-disable-line camelcase
this.goToMfa();
}
};
@@ -251,7 +244,6 @@ export default class Login extends PureComponent {
getLoginErrorMessage = () => {
return (
this.getServerErrorForLogin() ||
this.props.checkMfaRequest.error ||
this.state.error
);
};
@@ -265,6 +257,9 @@ export default class Login extends PureComponent {
if (!errorId) {
return error.message;
}
if (mfaExpectedErrors.includes(errorId) && !getMfaPreflightDone()) {
return null;
}
if (
errorId === 'store.sql_user.get_for_login.app_error' ||
errorId === 'ent.ldap.do_login.user_not_registered.app_error'

View File

@@ -24,6 +24,7 @@ import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_lo
import {GlobalStyles} from 'app/styles';
import {preventDoubleTap} from 'app/utils/tap';
import {t} from 'app/utils/i18n';
import {setMfaPreflightDone} from 'app/utils/security';
export default class Mfa extends PureComponent {
static propTypes = {
@@ -97,7 +98,7 @@ export default class Mfa extends PureComponent {
});
return;
}
setMfaPreflightDone(true);
this.props.actions.login(this.props.loginId, this.props.password, this.state.token);
});

View File

@@ -72,7 +72,7 @@ export default class Notification extends PureComponent {
/>
);
if (data.from_webhook && config.EnablePostIconOverride === 'true') {
if (data.from_webhook && config.EnablePostIconOverride === 'true' && data.use_user_icon !== 'true') {
const wsIcon = data.override_icon_url ? {uri: data.override_icon_url} : webhookIcon;
icon = (
<Image

View File

@@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getChannel as getChannelAction, joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {getChannel as getChannelAction, joinChannel} from 'mattermost-redux/actions/channels';
import {getPostsAfter, getPostsBefore, getPostThread, selectPost} from 'mattermost-redux/actions/posts';
import {makeGetChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {makeGetPostIdsAroundPost, getPost} from 'mattermost-redux/selectors/entities/posts';
@@ -68,8 +68,6 @@ function mapDispatchToProps(dispatch) {
handleTeamChange,
joinChannel,
loadThreadIfNecessary,
markChannelAsRead,
markChannelAsViewed,
selectPost,
setChannelDisplayName,
setChannelLoading,

View File

@@ -4,7 +4,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
Text,
TouchableOpacity,
View,
@@ -53,8 +52,6 @@ export default class Permalink extends PureComponent {
handleTeamChange: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
loadThreadIfNecessary: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
selectPost: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired,
@@ -232,10 +229,8 @@ export default class Permalink extends PureComponent {
const {
handleSelectChannel,
handleTeamChange,
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed,
} = actions;
actions.selectPost('');
@@ -266,19 +261,12 @@ export default class Permalink extends PureComponent {
}
if (channelTeamId && currentTeamId !== channelTeamId) {
handleTeamChange(channelTeamId, false);
handleTeamChange(channelTeamId);
}
setChannelLoading(channelId !== currentChannelId);
setChannelDisplayName(channelDisplayName);
handleSelectChannel(channelId);
InteractionManager.runAfterInteractions(async () => {
markChannelAsRead(channelId, currentChannelId);
if (channelId !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
});
}
};
@@ -311,9 +299,9 @@ export default class Permalink extends PureComponent {
if (!channelId) {
const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null;
focusChannelId = focusedPost ? focusedPost.channel_id : '';
if (focusChannelId && !this.props.myMembers[focusChannelId]) {
if (focusChannelId) {
const {data: channel} = await actions.getChannel(focusChannelId);
if (channel && channel.type === General.OPEN_CHANNEL) {
if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) {
await actions.joinChannel(currentUserId, channel.team_id, channel.id);
}
}

View File

@@ -28,8 +28,6 @@ describe('Permalink', () => {
handleTeamChange: jest.fn(),
joinChannel: jest.fn(),
loadThreadIfNecessary: jest.fn(),
markChannelAsRead: jest.fn(),
markChannelAsViewed: jest.fn(),
selectPost: jest.fn(),
setChannelDisplayName: jest.fn(),
setChannelLoading: jest.fn(),

View File

@@ -9,12 +9,10 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import SlideUpPanel from 'app/components/slide_up_panel';
import {BOTTOM_MARGIN} from 'app/components/slide_up_panel/slide_up_panel';
import DeviceTypes from 'app/constants/device';
import {OPTION_HEIGHT, getInitialPosition} from './post_options_utils';
import PostOption from './post_option';
const OPTION_HEIGHT = 50;
export default class PostOptions extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@@ -401,14 +399,14 @@ export default class PostOptions extends PureComponent {
const {deviceHeight} = this.props;
const options = this.getPostOptions();
const marginFromTop = deviceHeight - BOTTOM_MARGIN - ((options.length + 1) * OPTION_HEIGHT);
const initialPosition = DeviceTypes.IS_IPHONE_X ? 280 : 305;
const initialPosition = getInitialPosition(deviceHeight, marginFromTop);
return (
<View style={style.container}>
<SlideUpPanel
allowStayMiddle={false}
ref={this.refSlideUpPanel}
marginFromTop={marginFromTop}
marginFromTop={marginFromTop > 0 ? marginFromTop : 0}
onRequestClose={this.close}
initialPosition={initialPosition}
>

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const OPTION_HEIGHT = 50;
const BOTTOM_HEIGHT = 18;
export const MAX_INITIAL_POSITION_MULTIPLIER = 0.75;
export function getInitialPosition(deviceHeight, marginFromTop) {
const computedSlidePanelHeight = deviceHeight - marginFromTop;
const maxInitialPosition = deviceHeight * MAX_INITIAL_POSITION_MULTIPLIER;
if (computedSlidePanelHeight <= maxInitialPosition) {
// Show all options to the user
return computedSlidePanelHeight;
}
const optionHeightWithBorder = OPTION_HEIGHT + 1;
// Partially show options to user with the first hidden option in mid appearance
// to indicate that are still option/s available on slide up
let adjustedInitialPosition = computedSlidePanelHeight - BOTTOM_HEIGHT - (optionHeightWithBorder / 2);
while (adjustedInitialPosition > maxInitialPosition) {
adjustedInitialPosition -= optionHeightWithBorder;
}
return adjustedInitialPosition;
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getInitialPosition, MAX_INITIAL_POSITION_MULTIPLIER} from './post_options_utils';
describe('should match return value of getInitialPosition', () => {
const testCases = [
{
input: {deviceHeight: 600, marginFromTop: 400},
output: 200,
}, {
input: {deviceHeight: 600, marginFromTop: 300},
output: 300,
}, {
input: {deviceHeight: 600, marginFromTop: 50},
output: 404.5,
}, {
input: {deviceHeight: 1000, marginFromTop: 250},
output: 750,
}, {
input: {deviceHeight: 1000, marginFromTop: 150},
output: 704.5,
}, {
input: {deviceHeight: 1000, marginFromTop: 400},
output: 600,
},
];
for (const testCase of testCases) {
const {input, output} = testCase;
const maxInitialPosition = input.deviceHeight * MAX_INITIAL_POSITION_MULTIPLIER;
test('should match initial position', () => {
const initialPosition = getInitialPosition(input.deviceHeight, input.marginFromTop);
expect(initialPosition).toEqual(output);
// should not exceed maximum initial position at 75% of screen height
expect(initialPosition).toBeLessThanOrEqual(maxInitialPosition);
});
}
});

View File

@@ -34,7 +34,7 @@ exports[`ReactionList should match snapshot 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -106,7 +106,7 @@ exports[`ReactionList should match snapshot 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -184,7 +184,7 @@ Array [
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -256,7 +256,7 @@ Array [
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],

View File

@@ -29,7 +29,7 @@ export default class ReactionList extends PureComponent {
getMissingProfilesByIds: PropTypes.func.isRequired,
}).isRequired,
navigator: PropTypes.object,
reactions: PropTypes.array.isRequired,
reactions: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
teammateNameDisplay: PropTypes.string,
userProfiles: PropTypes.array,

View File

@@ -18,7 +18,7 @@ describe('ReactionList', () => {
},
allUserIds: ['user_id_1', 'user_id_2'],
navigator: {setOnNavigatorEvent: jest.fn()},
reactions: [{emoji_name: 'smile', user_id: 'user_id_1'}, {emoji_name: '+1', user_id: 'user_id_2'}],
reactions: {'user_id_1-smile': {emoji_name: 'smile', user_id: 'user_id_1'}, 'user_id_2-+1': {emoji_name: '+1', user_id: 'user_id_2'}},
theme: Preferences.THEMES.default,
teammateNameDisplay: 'username',
userProfiles: [{id: 'user_id_1', username: 'username_1'}, {id: 'user_id_2', username: 'username_2'}],

View File

@@ -8,7 +8,7 @@ import {getPing, resetPing, setServerVersion} from 'mattermost-redux/actions/gen
import {login} from 'mattermost-redux/actions/users';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import {getSession, handleSuccessfulLogin} from 'app/actions/views/login';
import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login';
import {loadConfigAndLicense} from 'app/actions/views/root';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import getClientUpgrade from 'app/selectors/client_upgrade';
@@ -39,7 +39,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
handleSuccessfulLogin,
getPing,
getSession,
scheduleExpiredNotification,
handleServerUrlChanged,
loadConfigAndLicense,
login,

View File

@@ -28,7 +28,6 @@ import FormattedText from 'app/components/formatted_text';
import {UpgradeTypes} from 'app/constants';
import fetchConfig from 'app/fetch_preconfig';
import mattermostBucket from 'app/mattermost_bucket';
import PushNotifications from 'app/push_notifications';
import {GlobalStyles} from 'app/styles';
import checkUpgradeType from 'app/utils/client_upgrade';
import {isValidUrl, stripTrailingSlashes} from 'app/utils/url';
@@ -44,7 +43,7 @@ export default class SelectServer extends PureComponent {
getPing: PropTypes.func.isRequired,
handleServerUrlChanged: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
getSession: PropTypes.func.isRequired,
scheduleExpiredNotification: PropTypes.func.isRequired,
loadConfigAndLicense: PropTypes.func.isRequired,
login: PropTypes.func.isRequired,
resetPing: PropTypes.func.isRequired,
@@ -189,7 +188,7 @@ export default class SelectServer extends PureComponent {
if (LocalConfig.ExperimentalClientSideCertEnable && Platform.OS === 'ios') {
RNFetchBlob.cba.selectCertificate((certificate) => {
if (certificate) {
mattermostBucket.setPreference('cert', certificate, LocalConfig.AppGroupId);
mattermostBucket.setPreference('cert', certificate);
window.fetch = new RNFetchBlob.polyfill.Fetch({
auto: true,
certificate,
@@ -203,7 +202,7 @@ export default class SelectServer extends PureComponent {
});
handleLoginOptions = (props = this.props) => {
const {intl} = this.context;
const {formatMessage} = this.context.intl;
const {config, license} = props;
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
const gitlabEnabled = config.EnableSignUpWithGitLab === 'true';
@@ -217,10 +216,10 @@ export default class SelectServer extends PureComponent {
let title;
if (options) {
screen = 'LoginOptions';
title = intl.formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'});
title = formatMessage({id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'});
} else {
screen = 'Login';
title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'});
title = formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'});
}
this.props.actions.resetPing();
@@ -251,12 +250,12 @@ export default class SelectServer extends PureComponent {
};
handleShowClientUpgrade = (upgradeType) => {
const {intl} = this.context;
const {formatMessage} = this.context.intl;
const {theme} = this.props;
this.props.navigator.push({
screen: 'ClientUpgrade',
title: intl.formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}),
title: formatMessage({id: 'mobile.client_upgrade', defaultMessage: 'Client Upgrade'}),
backButtonTitle: '',
navigatorStyle: {
navBarHidden: LocalConfig.AutoSelectServerUrl,
@@ -283,26 +282,13 @@ export default class SelectServer extends PureComponent {
};
loginWithCertificate = async () => {
const {intl, navigator} = this.props;
const {navigator} = this.props;
tracker.initialLoad = Date.now();
await this.props.actions.login('credential', 'password');
await this.props.actions.handleSuccessfulLogin();
const expiresAt = await this.props.actions.getSession();
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message: intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
}),
userInfo: {
localNotification: true,
},
});
}
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
@@ -335,6 +321,7 @@ export default class SelectServer extends PureComponent {
});
Client4.setUrl(url);
Client4.online = true;
handleServerUrlChanged(url);
let cancel = false;
@@ -380,11 +367,18 @@ export default class SelectServer extends PureComponent {
});
};
scheduleSessionExpiredNotification = () => {
const {intl} = this.context;
const {actions} = this.props;
actions.scheduleExpiredNotification(intl);
};
selectCertificate = () => {
const url = this.getUrl();
RNFetchBlob.cba.selectCertificate((certificate) => {
if (certificate) {
mattermostBucket.setPreference('cert', certificate, LocalConfig.AppGroupId);
mattermostBucket.setPreference('cert', certificate);
fetchConfig().then(() => {
this.pingServer(url, true);
});

View File

@@ -6,10 +6,8 @@ import {connect} from 'react-redux';
import {handleTeamChange} from 'app/actions/views/select_team';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams, joinTeam} from 'mattermost-redux/actions/teams';
import {logout} from 'mattermost-redux/actions/users';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentLocale} from 'app/selectors/i18n';
@@ -30,7 +28,6 @@ function mapStateToProps(state) {
return {
teamsRequest: state.requests.teams.getTeams,
teams: Object.values(getJoinableTeams(state)).sort(sortTeams),
currentChannelId: getCurrentChannelId(state),
};
}
@@ -41,7 +38,6 @@ function mapDispatchToProps(dispatch) {
handleTeamChange,
joinTeam,
logout,
markChannelAsRead,
}, dispatch),
};
}

View File

@@ -47,9 +47,7 @@ export default class SelectTeam extends PureComponent {
handleTeamChange: PropTypes.func.isRequired,
joinTeam: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
}).isRequired,
currentChannelId: PropTypes.string,
currentUrl: PropTypes.string.isRequired,
navigator: PropTypes.object,
userWithoutTeams: PropTypes.bool,
@@ -143,17 +141,12 @@ export default class SelectTeam extends PureComponent {
onSelectTeam = async (team) => {
this.setState({joining: true});
const {currentChannelId, userWithoutTeams} = this.props;
const {userWithoutTeams} = this.props;
const {
joinTeam,
handleTeamChange,
markChannelAsRead,
} = this.props.actions;
if (currentChannelId) {
markChannelAsRead(currentChannelId);
}
const {error} = await joinTeam(team.invite_id, team.id);
if (error) {
Alert.alert(error.message);

View File

@@ -29,7 +29,6 @@ describe('SelectTeam', () => {
handleTeamChange: jest.fn(),
joinTeam: jest.fn(),
logout: jest.fn(),
markChannelAsRead: jest.fn(),
};
const baseProps = {

View File

@@ -18,6 +18,7 @@ import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
import SettingsItem from 'app/screens/settings/settings_item';
import StatusBar from 'app/components/status_bar';
import {t} from 'app/utils/i18n';
import {deleteFileCache, getFileCacheSize} from 'app/utils/file';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -46,8 +47,8 @@ class AdvancedSettings extends PureComponent {
const {formatMessage} = this.props.intl;
Alert.alert(
formatMessage({id: 'mobile.advanced_settings.delete_title', defaultMessage: 'Delete Documents & Data'}),
formatMessage({id: 'mobile.advanced_settings.delete_message', defaultMessage: '\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n'}),
formatMessage({id: t('mobile.advanced_settings.delete_title'), defaultMessage: 'Delete Documents & Data'}),
formatMessage({id: t('mobile.advanced_settings.delete_message'), defaultMessage: '\nThis will reset all offline data and restart the app. You will be automatically logged back in once the app restarts.\n'}),
[{
text: formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'}),
style: 'cancel',

View File

@@ -25,8 +25,8 @@ function mapStateToProps(state) {
state,
Preferences.CATEGORY_NOTIFICATIONS,
Preferences.EMAIL_INTERVAL,
Preferences.INTERVAL_NEVER.toString(),
) || '0';
Preferences.INTERVAL_NOT_SET.toString(),
);
return {
enableEmailBatching,

View File

@@ -28,7 +28,7 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase {
handleClose = () => {
this.setState({
newInterval: this.state.interval,
newInterval: this.state.emailInterval,
showEmailNotificationsModal: false,
});
}
@@ -47,11 +47,11 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase {
sendEmailNotifications,
theme,
} = this.props;
const {interval} = this.state;
const {newInterval} = this.state;
let i18nId;
let i18nMessage;
if (sendEmailNotifications) {
switch (interval) {
switch (newInterval) {
case Preferences.INTERVAL_IMMEDIATE.toString():
i18nId = t('user.settings.notifications.email.immediately');
i18nMessage = 'Immediately';
@@ -180,7 +180,7 @@ class NotificationSettingsEmailAndroid extends NotificationSettingsEmailBase {
{sendEmailNotifications &&
<RadioButtonGroup
name='emailSettings'
onSelect={this.setEmailNotifications}
onSelect={this.setEmailInterval}
options={emailOptions}
/>
}

View File

@@ -12,6 +12,12 @@ import RadioButtonGroup from 'app/components/radio_button';
import NotificationSettingsEmailAndroid from './notification_settings_email.android.js';
jest.mock('Platform', () => {
const Platform = require.requireActual('Platform');
Platform.OS = 'android';
return Platform;
});
describe('NotificationSettingsEmailAndroid', () => {
const baseProps = {
currentUser: {id: 'current_user_id'},
@@ -50,20 +56,20 @@ describe('NotificationSettingsEmailAndroid', () => {
expect(wrapper.instance().renderEmailNotificationsModal(style)).toMatchSnapshot();
});
test('should match state on setEmailNotifications', () => {
test('should match state on setEmailInterval', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsEmailAndroid {...baseProps}/>
);
wrapper.setState({email: 'false', interval: '0'});
wrapper.instance().setEmailNotifications('30');
expect(wrapper.state({email: 'true', interval: '30'}));
wrapper.setState({interval: '0'});
wrapper.instance().setEmailInterval('30');
expect(wrapper.state({interval: '30'}));
wrapper.instance().setEmailNotifications('0');
expect(wrapper.state({email: 'false', interval: '0'}));
wrapper.instance().setEmailInterval('0');
expect(wrapper.state({interval: '0'}));
wrapper.instance().setEmailNotifications('3600');
expect(wrapper.state({email: 'true', interval: '3600'}));
wrapper.instance().setEmailInterval('3600');
expect(wrapper.state({interval: '3600'}));
});
test('should match state on select of RadioButtonGroup', () => {
@@ -139,4 +145,22 @@ describe('NotificationSettingsEmailAndroid', () => {
wrapper.instance().showEmailModal();
expect(wrapper.state('showEmailNotificationsModal')).toEqual(true);
});
test('should not save preference on back button on Android', () => {
const wrapper = shallowWithIntl(
<NotificationSettingsEmailAndroid {...baseProps}/>
);
const instance = wrapper.instance();
instance.saveEmailNotifyProps = jest.fn();
// should not save preference on back button on Android
// saving email preference on Android is done via Save button
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(0);
wrapper.setState({newInterval: '0'});
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(0);
});
});

View File

@@ -28,7 +28,7 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
siteName,
theme,
} = this.props;
const {interval} = this.state;
const {newInterval} = this.state;
const style = getStyleSheet(theme);
return (
@@ -50,10 +50,10 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
defaultMessage='Immediately'
/>
)}
action={this.setEmailNotifications}
action={this.setEmailInterval}
actionType='select'
actionValue={Preferences.INTERVAL_IMMEDIATE.toString()}
selected={interval === Preferences.INTERVAL_IMMEDIATE.toString()}
selected={newInterval === Preferences.INTERVAL_IMMEDIATE.toString()}
theme={theme}
/>
<View style={style.separator}/>
@@ -66,10 +66,10 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
defaultMessage='Every 15 minutes'
/>
)}
action={this.setEmailNotifications}
action={this.setEmailInterval}
actionType='select'
actionValue={Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
selected={interval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
selected={newInterval === Preferences.INTERVAL_FIFTEEN_MINUTES.toString()}
theme={theme}
/>
<View style={style.separator}/>
@@ -80,10 +80,10 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
defaultMessage='Every hour'
/>
)}
action={this.setEmailNotifications}
action={this.setEmailInterval}
actionType='select'
actionValue={Preferences.INTERVAL_HOUR.toString()}
selected={interval === Preferences.INTERVAL_HOUR.toString()}
selected={newInterval === Preferences.INTERVAL_HOUR.toString()}
theme={theme}
/>
<View style={style.separator}/>
@@ -96,10 +96,10 @@ class NotificationSettingsEmailIos extends NotificationSettingsEmailBase {
defaultMessage='Never'
/>
)}
action={this.setEmailNotifications}
action={this.setEmailInterval}
actionType='select'
actionValue={Preferences.INTERVAL_NEVER.toString()}
selected={interval === Preferences.INTERVAL_NEVER.toString()}
selected={newInterval === Preferences.INTERVAL_NEVER.toString()}
theme={theme}
/>
</View>

View File

@@ -12,6 +12,12 @@ import SectionItem from 'app/screens/settings/section_item';
import NotificationSettingsEmailIos from './notification_settings_email.ios.js';
jest.mock('Platform', () => {
const Platform = require.requireActual('Platform');
Platform.OS = 'ios';
return Platform;
});
jest.mock('app/utils/theme', () => {
const original = require.requireActual('app/utils/theme');
return {
@@ -43,16 +49,23 @@ describe('NotificationSettingsEmailIos', () => {
expect(wrapper.instance().renderEmailSection()).toMatchSnapshot();
});
test('should call saveEmailNotifyProps on onNavigatorEvent', () => {
test('should save preference on back button only if email interval has changed', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>
);
const instance = wrapper.instance();
instance.saveEmailNotifyProps = jest.fn();
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(instance.saveEmailNotifyProps).toHaveBeenCalledTimes(1);
// should not save preference if email interval has not changed.
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(0);
expect(baseProps.actions.savePreferences).toHaveBeenCalledTimes(0);
// should save preference if email interval has changed.
wrapper.setState({newInterval: '0'});
instance.onNavigatorEvent({type: 'ScreenChangedEvent', id: 'willDisappear'});
expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(1);
expect(baseProps.actions.savePreferences).toHaveBeenCalledTimes(1);
});
test('should call actions.updateMe and actions.savePreferences on saveEmailNotifyProps', () => {
@@ -73,20 +86,20 @@ describe('NotificationSettingsEmailIos', () => {
expect(savePreferences).toBeCalledWith('current_user_id', [{category: 'notifications', name: 'email_interval', user_id: 'current_user_id', value: 30}]);
});
test('should match state on setEmailNotifications', () => {
test('should match state on setEmailInterval', () => {
const wrapper = shallow(
<NotificationSettingsEmailIos {...baseProps}/>
);
wrapper.setState({email: 'false', interval: '0'});
wrapper.instance().setEmailNotifications('30');
expect(wrapper.state({email: 'true', interval: '30'}));
wrapper.setState({interval: '0'});
wrapper.instance().setEmailInterval('30');
expect(wrapper.state({interval: '30'}));
wrapper.instance().setEmailNotifications('0');
expect(wrapper.state({email: 'false', interval: '0'}));
wrapper.instance().setEmailInterval('0');
expect(wrapper.state({interval: '0'}));
wrapper.instance().setEmailNotifications('3600');
expect(wrapper.state({email: 'true', interval: '3600'}));
wrapper.instance().setEmailInterval('3600');
expect(wrapper.state({interval: '3600'}));
});
test('should match state on action of SectionItem', () => {

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {PureComponent} from 'react';
import {Platform} from 'react-native';
import PropTypes from 'prop-types';
import {Preferences} from 'mattermost-redux/constants';
@@ -29,17 +30,18 @@ export default class NotificationSettingsEmailBase extends PureComponent {
super(props);
const {
currentUser,
emailInterval,
enableEmailBatching,
navigator,
sendEmailNotifications,
} = props;
const interval = this.computeEmailInterval(sendEmailNotifications, enableEmailBatching, emailInterval);
const notifyProps = getNotificationProps(currentUser);
this.state = {
interval,
newInterval: interval,
emailInterval,
newInterval: this.computeEmailInterval(notifyProps?.email === 'true' && sendEmailNotifications, enableEmailBatching, emailInterval),
showEmailNotificationsModal: false,
};
@@ -52,6 +54,7 @@ export default class NotificationSettingsEmailBase extends PureComponent {
}
const {
currentUser,
sendEmailNotifications,
enableEmailBatching,
emailInterval,
@@ -62,16 +65,17 @@ export default class NotificationSettingsEmailBase extends PureComponent {
this.props.enableEmailBatching !== enableEmailBatching ||
this.props.emailInterval !== emailInterval
) {
const interval = this.computeEmailInterval(sendEmailNotifications, enableEmailBatching, emailInterval);
const notifyProps = getNotificationProps(currentUser);
this.setState({
interval,
newInterval: interval,
emailInterval,
newInterval: this.computeEmailInterval(notifyProps?.email === 'true' && sendEmailNotifications, enableEmailBatching, emailInterval),
});
}
}
onNavigatorEvent = (event) => {
if (event.type === 'ScreenChangedEvent') {
if (Platform.OS === 'ios' && event.type === 'ScreenChangedEvent') {
switch (event.id) {
case 'willDisappear':
this.saveEmailNotifyProps();
@@ -80,30 +84,31 @@ export default class NotificationSettingsEmailBase extends PureComponent {
}
};
setEmailNotifications = (value) => {
const {sendEmailNotifications} = this.props;
let email = 'false';
if (sendEmailNotifications && value !== Preferences.INTERVAL_NEVER.toString()) {
email = 'true';
}
this.setState({
email,
interval: value,
newInterval: value,
});
setEmailInterval = (value) => {
this.setState({newInterval: value});
};
saveEmailNotifyProps = () => {
const {actions, currentUser} = this.props;
const {email, newInterval} = this.state;
const {emailInterval, newInterval} = this.state;
const notifyProps = getNotificationProps(currentUser);
actions.updateMe({notify_props: {...notifyProps, email}});
if (emailInterval !== newInterval) {
const {
actions,
currentUser,
sendEmailNotifications,
} = this.props;
const emailInterval = {category: Preferences.CATEGORY_NOTIFICATIONS, user_id: currentUser.id, name: Preferences.EMAIL_INTERVAL, value: newInterval};
actions.savePreferences(currentUser.id, [emailInterval]);
let email = 'false';
if (sendEmailNotifications && newInterval !== Preferences.INTERVAL_NEVER.toString()) {
email = 'true';
}
const notifyProps = getNotificationProps(currentUser);
actions.updateMe({notify_props: {...notifyProps, email}});
const emailIntervalPreference = {category: Preferences.CATEGORY_NOTIFICATIONS, user_id: currentUser.id, name: Preferences.EMAIL_INTERVAL, value: newInterval};
actions.savePreferences(currentUser.id, [emailIntervalPreference]);
}
};
computeEmailInterval = (sendEmailNotifications, enableEmailBatching, emailInterval) => {

View File

@@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getSession, handleSuccessfulLogin} from 'app/actions/views/login';
import {handleSuccessfulLogin, scheduleExpiredNotification} from 'app/actions/views/login';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setStoreFromLocalData} from 'mattermost-redux/actions/general';
@@ -21,7 +21,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getSession,
scheduleExpiredNotification,
handleSuccessfulLogin,
setStoreFromLocalData,
}, dispatch),

View File

@@ -18,7 +18,6 @@ import {Client4} from 'mattermost-redux/client';
import {ViewTypes} from 'app/constants';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import PushNotifications from 'app/push_notifications';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
@@ -66,7 +65,7 @@ class SSO extends PureComponent {
serverUrl: PropTypes.string.isRequired,
ssoType: PropTypes.string.isRequired,
actions: PropTypes.shape({
getSession: PropTypes.func.isRequired,
scheduleExpiredNotification: PropTypes.func.isRequired,
handleSuccessfulLogin: PropTypes.func.isRequired,
setStoreFromLocalData: PropTypes.func.isRequired,
}).isRequired,
@@ -104,22 +103,11 @@ class SSO extends PureComponent {
});
};
goToLoadTeam = (expiresAt) => {
const {intl, navigator} = this.props;
goToChannel = () => {
const {navigator} = this.props;
tracker.initialLoad = Date.now();
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message: intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
}),
userInfo: {
localNotification: true,
},
});
}
this.scheduleSessionExpiredNotification();
navigator.resetTo({
screen: 'Channel',
@@ -182,7 +170,6 @@ class SSO extends PureComponent {
if (token) {
this.setState({renderWebView: false});
const {
getSession,
handleSuccessfulLogin,
setStoreFromLocalData,
} = this.props.actions;
@@ -190,8 +177,7 @@ class SSO extends PureComponent {
Client4.setToken(token);
setStoreFromLocalData({url: Client4.getUrl(), token}).
then(handleSuccessfulLogin).
then(getSession).
then(this.goToLoadTeam).
then(this.goToChannel).
catch(this.onLoadEndError);
} else if (this.webView && !this.state.error) {
this.webView.injectJavaScript(postMessageJS);
@@ -205,6 +191,12 @@ class SSO extends PureComponent {
this.setState({error: e.message});
};
scheduleSessionExpiredNotification = () => {
const {actions, intl} = this.props;
actions.scheduleExpiredNotification(intl);
};
renderLoading = () => {
return <Loading/>;
};
@@ -240,7 +232,7 @@ class SSO extends PureComponent {
renderLoading={this.renderLoading}
injectedJavaScript={jsCode}
onLoadEnd={this.onLoadEnd}
onMessage={messagingEnabled && this.onMessage}
onMessage={messagingEnabled ? this.onMessage : null}
useWebKit={true}
/>
);

View File

@@ -111,15 +111,15 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -132,7 +132,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -359,19 +359,19 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -384,7 +384,7 @@ exports[`TermsOfService should enable/disable navigator buttons on setNavigatorB
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -664,15 +664,15 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
@@ -685,7 +685,7 @@ exports[`TermsOfService should match snapshot on enableNavigatorLogout 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],

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