forked from Ivasoft/mattermost-mobile
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa0c97e3f | ||
|
|
badaaef592 | ||
|
|
96571c0f9c | ||
|
|
ebe776e8b4 | ||
|
|
242c6cc171 | ||
|
|
e73aa8662f | ||
|
|
aa80adfacc | ||
|
|
2f7a12bc16 | ||
|
|
779306ae6f | ||
|
|
c4d4009fad | ||
|
|
35efa47b35 | ||
|
|
c7dc992ac0 | ||
|
|
d22422840b | ||
|
|
cc3ce0e509 | ||
|
|
fbd5c34c5e | ||
|
|
9485d4e88d | ||
|
|
2c5252e386 | ||
|
|
aa7054ee1e | ||
|
|
eb0fd4f3ad | ||
|
|
490e2e550e | ||
|
|
fe46802aed | ||
|
|
9253134612 | ||
|
|
2f8e4116a1 | ||
|
|
5f0a045046 | ||
|
|
f53a7c26b8 | ||
|
|
d012bead87 | ||
|
|
7a933bef29 | ||
|
|
1dcb8f2b25 | ||
|
|
ee501a4b50 | ||
|
|
830113d6b5 | ||
|
|
a6ad628c4f | ||
|
|
a34ff7b247 | ||
|
|
4f9d0c68b5 | ||
|
|
cf7f92ea47 | ||
|
|
e1b6f174f9 | ||
|
|
42a4806d32 | ||
|
|
7bb6821ea6 | ||
|
|
c34a706b62 | ||
|
|
787d5e4208 | ||
|
|
f376d0b07b | ||
|
|
fb75bf3916 | ||
|
|
a531760e64 | ||
|
|
4c9ad4a424 | ||
|
|
0de0920974 | ||
|
|
d69ef1b777 | ||
|
|
883752c9f2 | ||
|
|
7f6243f347 | ||
|
|
c47980e0f2 | ||
|
|
7369434048 | ||
|
|
24a32867b1 | ||
|
|
758743580d | ||
|
|
920e46f9a1 | ||
|
|
fac489a5d0 | ||
|
|
cbb9a2b749 | ||
|
|
b382f81cf6 | ||
|
|
66cd2a98ac | ||
|
|
317e3b14ab | ||
|
|
87af5404ae | ||
|
|
4f3679e09d | ||
|
|
976a29ed29 | ||
|
|
ffef7e2bcf | ||
|
|
e5e741840e | ||
|
|
83ec54b8c8 | ||
|
|
226bfa8dd2 | ||
|
|
1ebf6fc0c2 | ||
|
|
d40b1da1d0 | ||
|
|
b24c97b7e9 | ||
|
|
5d61ae3342 | ||
|
|
ccc33cbf39 | ||
|
|
43a0d25636 | ||
|
|
860d4c582f | ||
|
|
c930bf85af | ||
|
|
97e2647eae | ||
|
|
24ff55f4c0 | ||
|
|
95fb8a9348 | ||
|
|
2ded5182f3 | ||
|
|
83b0cfb9ba | ||
|
|
d29998957b | ||
|
|
7e182f6022 | ||
|
|
7fa7cf4364 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
assets/override
|
||||
dist
|
||||
build-ios
|
||||
*.zip
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
|
||||
91
Makefile
91
Makefile
@@ -2,7 +2,7 @@
|
||||
.PHONY: check-style
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build build-ios build-android unsigned-ios unsigned-android
|
||||
.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
|
||||
|
||||
93
NOTICE.txt
93
NOTICE.txt
@@ -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:
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,7 +314,6 @@ export default class App {
|
||||
break;
|
||||
}
|
||||
|
||||
this.setStartAppFromPushNotification(false);
|
||||
this.setAppStarted(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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}
|
||||
|
||||
70
app/components/post_body/post_body.test.js
Normal file
70
app/components/post_body/post_body.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -184,7 +184,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
if (!openGraphData) {
|
||||
if (!openGraphData && metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class Reactions extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
const {actions, postId, reactions} = this.props;
|
||||
if (reactions) {
|
||||
if (!reactions) {
|
||||
actions.getReactionsForPost(postId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
4
app/constants/screen.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const THREAD = 'thread';
|
||||
@@ -87,6 +87,7 @@ const ViewTypes = keyMirror({
|
||||
|
||||
SELECTED_ACTION_MENU: null,
|
||||
SUBMIT_ATTACHMENT_MENU_ACTION: null,
|
||||
SELECT_CHANNEL_WITH_MEMBER: null,
|
||||
});
|
||||
|
||||
export default {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -123,10 +123,6 @@ const state = {
|
||||
},
|
||||
},
|
||||
users: {
|
||||
checkMfa: {
|
||||
status: 'not_started',
|
||||
error: null,
|
||||
},
|
||||
login: {
|
||||
status: 'not_started',
|
||||
error: null,
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
27
app/screens/post_options/post_options_utils.js
Normal file
27
app/screens/post_options/post_options_utils.js
Normal 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;
|
||||
}
|
||||
41
app/screens/post_options/post_options_utils.test.js
Normal file
41
app/screens/post_options/post_options_utils.test.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'}],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -29,7 +29,6 @@ describe('SelectTeam', () => {
|
||||
handleTeamChange: jest.fn(),
|
||||
joinTeam: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
markChannelAsRead: jest.fn(),
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user