Compare commits
67 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b18570aa23 | ||
|
|
f70a8508a3 | ||
|
|
dad72467dd | ||
|
|
eeb73700b8 | ||
|
|
735aa6dd0b | ||
|
|
eda896ec70 | ||
|
|
bc0c31d707 | ||
|
|
886ef3f8c9 | ||
|
|
20ac82f4ba | ||
|
|
67d09a7303 | ||
|
|
ef2ec25670 | ||
|
|
e27d3ed463 | ||
|
|
d8a76946f3 | ||
|
|
0b74be530f | ||
|
|
dfd011aebc | ||
|
|
7859b85391 | ||
|
|
440ca4ed24 | ||
|
|
f49c2b430c | ||
|
|
5c654364c1 | ||
|
|
584efe2b12 | ||
|
|
55d029bde3 | ||
|
|
783e72a8d0 | ||
|
|
0764523b92 | ||
|
|
839b21a3b3 | ||
|
|
212f2f5db6 | ||
|
|
47debc68c1 | ||
|
|
b878584020 | ||
|
|
86f2b0a7b9 | ||
|
|
89e723b927 | ||
|
|
78058c2bda | ||
|
|
b1324bcf13 | ||
|
|
7668670884 | ||
|
|
de276c7d93 | ||
|
|
2d989a59e7 | ||
|
|
15125ba098 | ||
|
|
c318f92470 | ||
|
|
d5bf5bec78 | ||
|
|
b11cf8e51c | ||
|
|
de7b88beb2 | ||
|
|
76957c5ae4 | ||
|
|
5d887f067d | ||
|
|
dbd56671a0 | ||
|
|
316f409472 | ||
|
|
8ae81de8f4 | ||
|
|
12aebc6713 | ||
|
|
f9645e63e1 | ||
|
|
7590bb3063 | ||
|
|
69a2c58f5e | ||
|
|
7efb044aa9 | ||
|
|
25673ff7e0 | ||
|
|
78b23ae37e | ||
|
|
8ef6b35369 | ||
|
|
385a081f78 | ||
|
|
0377249592 | ||
|
|
caac14907e | ||
|
|
6fef6d6b92 | ||
|
|
64337b4851 | ||
|
|
713dd4e578 | ||
|
|
41ddeef2f7 | ||
|
|
00e05c5e8f | ||
|
|
a74fabcc98 | ||
|
|
bb9f96f409 | ||
|
|
3adec36c95 | ||
|
|
4c690b5578 | ||
|
|
39129fc6c4 | ||
|
|
370fa9b952 | ||
|
|
52e379ae51 |
@@ -1,500 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
executors:
|
||||
android:
|
||||
parameters:
|
||||
resource_class:
|
||||
default: large
|
||||
type: string
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: circleci/android:api-27-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
ios:
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "11.0.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
|
||||
commands:
|
||||
checkout-private:
|
||||
description: "Checkout the private repo with build env vars"
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "59:4d:99:5e:1c:6d:30:36:6d:60:76:88:ff:a7:ab:63"
|
||||
- run:
|
||||
name: Clone the mobile private repo
|
||||
command: git clone git@github.com:mattermost/mattermost-mobile-private.git ~/mattermost-mobile-private
|
||||
|
||||
fastlane-dependencies:
|
||||
description: "Get Fastlane dependencies"
|
||||
parameters:
|
||||
for:
|
||||
type: string
|
||||
steps:
|
||||
- ruby-setup
|
||||
- restore_cache:
|
||||
name: Restore Fastlane cache
|
||||
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Download Fastlane dependencies
|
||||
command: bundle install --path vendor/bundle
|
||||
- save_cache:
|
||||
name: Save Fastlane cache
|
||||
key: v1-gems-<< parameters.for >>-{{ checksum "fastlane/Gemfile.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- fastlane/vendor/bundle
|
||||
|
||||
gradle-dependencies:
|
||||
description: "Get Gradle dependencies"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Gradle cache
|
||||
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
- run:
|
||||
working_directory: android
|
||||
name: Download Gradle dependencies
|
||||
command: ./gradlew dependencies
|
||||
- save_cache:
|
||||
name: Save Gradle cache
|
||||
paths:
|
||||
- ~/.gradle
|
||||
key: v1-gradle-{{ checksum "android/build.gradle" }}-{{ checksum "android/app/build.gradle" }}
|
||||
|
||||
assets:
|
||||
description: "Generate app assets"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore assets cache
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Generate assets
|
||||
command: make dist/assets
|
||||
- save_cache:
|
||||
name: Save assets cache
|
||||
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
|
||||
paths:
|
||||
- dist
|
||||
|
||||
npm-dependencies:
|
||||
description: "Get JavaScript dependencies"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
- run:
|
||||
name: Getting JavaScript dependencies
|
||||
command: NODE_ENV=development npm install --ignore-scripts
|
||||
- save_cache:
|
||||
name: Save npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: "Run post install scripts"
|
||||
command: make post-install
|
||||
|
||||
pods-dependencies:
|
||||
description: "Get cocoapods dependencies"
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore cocoapods specs and pods
|
||||
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
|
||||
- run:
|
||||
name: Getting cocoapods dependencies
|
||||
working_directory: ios
|
||||
command: pod install
|
||||
- save_cache:
|
||||
name: Save cocoapods specs and pods cache
|
||||
key: v1-cocoapods-{{ checksum "ios/Podfile.lock" }}-{{ arch }}
|
||||
paths:
|
||||
- ios/Pods
|
||||
- ~/.cocoapods
|
||||
|
||||
build-android:
|
||||
description: "Build the android app"
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- checkout-private
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: android
|
||||
- gradle-dependencies
|
||||
- run:
|
||||
name: Append Keystore to build Android
|
||||
command: |
|
||||
cp ~/mattermost-mobile-private/android/${STORE_FILE} android/app/${STORE_FILE}
|
||||
echo "" | tee -a android/gradle.properties > /dev/null
|
||||
echo MATTERMOST_RELEASE_STORE_FILE=${STORE_FILE} | tee -a android/gradle.properties > /dev/null
|
||||
echo ${STORE_ALIAS} | tee -a android/gradle.properties > /dev/null
|
||||
echo ${STORE_PASSWORD} | tee -a android/gradle.properties > /dev/null
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build android
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane android build
|
||||
|
||||
build-ios:
|
||||
description: "Build the iOS app"
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build iOS
|
||||
no_output_timeout: 30m
|
||||
command: |
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install watchman
|
||||
bundle exec fastlane ios build
|
||||
|
||||
deploy-to-store:
|
||||
description: "Deploy build to store"
|
||||
parameters:
|
||||
task:
|
||||
type: string
|
||||
target:
|
||||
type: string
|
||||
file:
|
||||
type: string
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: <<parameters.task>>
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane <<parameters.target>> deploy file:$HOME/mattermost-mobile/<<parameters.file>>
|
||||
|
||||
persist:
|
||||
description: "Persist mattermost-mobile directory"
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: ~/
|
||||
paths:
|
||||
- mattermost-mobile*
|
||||
|
||||
save:
|
||||
description: "Save binaries artifacts"
|
||||
parameters:
|
||||
filename:
|
||||
type: string
|
||||
steps:
|
||||
- store_artifacts:
|
||||
path: ~/mattermost-mobile/<<parameters.filename>>
|
||||
|
||||
ruby-setup:
|
||||
steps:
|
||||
- run:
|
||||
name: Set Ruby Version
|
||||
command: echo "ruby-2.6.3" > ~/.ruby-version
|
||||
|
||||
jobs:
|
||||
test:
|
||||
working_directory: ~/mattermost-mobile
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- run:
|
||||
name: Check styles
|
||||
command: npm run check
|
||||
- run:
|
||||
name: Running Tests
|
||||
command: npm test
|
||||
- run:
|
||||
name: Check i18n
|
||||
command: make i18n-extract-ci
|
||||
|
||||
build-android-beta:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost_Beta.apk"
|
||||
|
||||
build-android-release:
|
||||
executor: android
|
||||
steps:
|
||||
- build-android
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost.apk"
|
||||
|
||||
build-android-pr:
|
||||
executor: android
|
||||
environment:
|
||||
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
|
||||
steps:
|
||||
- build-android
|
||||
- save:
|
||||
filename: "Mattermost_Beta.apk"
|
||||
|
||||
build-android-unsigned:
|
||||
executor: android
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: android
|
||||
- gradle-dependencies
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned android
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane android unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost-unsigned.apk"
|
||||
|
||||
build-ios-beta:
|
||||
executor: ios
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost_Beta.ipa"
|
||||
|
||||
build-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- build-ios
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost.ipa"
|
||||
|
||||
build-ios-pr:
|
||||
executor: ios
|
||||
environment:
|
||||
BRANCH_TO_BUILD: ${CIRCLE_BRANCH}
|
||||
steps:
|
||||
- build-ios
|
||||
- save:
|
||||
filename: "Mattermost_Beta.ipa"
|
||||
|
||||
build-ios-unsigned:
|
||||
executor: ios
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/mattermost-mobile
|
||||
- npm-dependencies
|
||||
- pods-dependencies
|
||||
- assets
|
||||
- fastlane-dependencies:
|
||||
for: ios
|
||||
- run:
|
||||
working_directory: fastlane
|
||||
name: Run fastlane to build unsigned iOS
|
||||
no_output_timeout: 30m
|
||||
command: bundle exec fastlane ios unsigned
|
||||
- persist
|
||||
- save:
|
||||
filename: "Mattermost-unsigned.ipa"
|
||||
|
||||
deploy-android-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: Mattermost.apk
|
||||
|
||||
deploy-android-beta:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to Google Play"
|
||||
target: android
|
||||
file: Mattermost_Beta.apk
|
||||
|
||||
deploy-ios-release:
|
||||
executor: ios
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: Mattermost.ipa
|
||||
|
||||
deploy-ios-beta:
|
||||
executor: ios
|
||||
steps:
|
||||
- ruby-setup
|
||||
- deploy-to-store:
|
||||
task: "Deploy to TestFlight"
|
||||
target: ios
|
||||
file: Mattermost_Beta.ipa
|
||||
|
||||
github-release:
|
||||
executor:
|
||||
name: android
|
||||
resource_class: medium
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/
|
||||
- run:
|
||||
name: Create GitHub release
|
||||
working_directory: fastlane
|
||||
command: bundle exec fastlane github
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- test
|
||||
|
||||
- build-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
- deploy-android-release:
|
||||
context: mattermost-mobile-android-release
|
||||
requires:
|
||||
- build-android-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-release-\d+$/
|
||||
|
||||
- build-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
- deploy-android-beta:
|
||||
context: mattermost-mobile-android-beta
|
||||
requires:
|
||||
- build-android-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-android-\d+$/
|
||||
- /^build-android-beta-\d+$/
|
||||
|
||||
- build-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
- deploy-ios-release:
|
||||
context: mattermost-mobile-ios-release
|
||||
requires:
|
||||
- build-ios-release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-release-\d+$/
|
||||
|
||||
- build-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-beta-\d+$/
|
||||
- deploy-ios-beta:
|
||||
context: mattermost-mobile-ios-beta
|
||||
requires:
|
||||
- build-ios-beta
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-beta-\d+$/
|
||||
|
||||
- build-android-pr:
|
||||
context: mattermost-mobile-android-pr
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^build-pr-.*/
|
||||
- build-ios-pr:
|
||||
context: mattermost-mobile-ios-pr
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^build-pr-.*/
|
||||
|
||||
- build-android-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- build-ios-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
requires:
|
||||
- build-android-unsigned
|
||||
- build-ios-unsigned
|
||||
filters:
|
||||
tags:
|
||||
only: /^v(\d+\.)(\d+\.)(\d+)(.*)?$/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
77
.flowconfig
@@ -5,24 +5,26 @@
|
||||
; Ignore "BUCK" generated dirs
|
||||
<PROJECT_ROOT>/\.buckd/
|
||||
|
||||
; Ignore unexpected extra "@providesModule"
|
||||
.*/node_modules/.*/node_modules/fbjs/.*
|
||||
|
||||
; Ignore duplicate module providers
|
||||
; For RN Apps installed via npm, "Libraries" folder is inside
|
||||
; "node_modules/react-native" but in the source repo it is in the root
|
||||
.*/Libraries/react-native/React.js
|
||||
|
||||
; Ignore polyfills
|
||||
node_modules/react-native/Libraries/polyfills/.*
|
||||
.*/Libraries/polyfills/.*
|
||||
|
||||
; These should not be required directly
|
||||
; require from fbjs/lib instead: require('fbjs/lib/warning')
|
||||
node_modules/warning/.*
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/LoadingView.js
|
||||
|
||||
[untyped]
|
||||
.*/node_modules/@react-native-community/cli/.*/.*
|
||||
; Ignore metro
|
||||
.*/node_modules/metro/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
node_modules/react-native/Libraries/react-native/react-native-interface.js
|
||||
node_modules/react-native/flow/
|
||||
node_modules/react-native/flow-github/
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
@@ -30,46 +32,39 @@ emoji=true
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
module.system=haste
|
||||
module.system.haste.use_name_reducers=true
|
||||
# get basename
|
||||
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
|
||||
# strip .js or .js.flow suffix
|
||||
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
|
||||
# strip .ios suffix
|
||||
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
|
||||
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
|
||||
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
|
||||
module.system.haste.paths.blacklist=.*/__tests__/.*
|
||||
module.system.haste.paths.blacklist=.*/__mocks__/.*
|
||||
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
|
||||
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
|
||||
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^react-native$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/react-native/react-native-implementation'
|
||||
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/node_modules/react-native/\1'
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '<PROJECT_ROOT>/node_modules/react-native/Libraries/Image/RelativeImageStub'
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.jsx
|
||||
module.file_ext=.json
|
||||
module.file_ext=.native.js
|
||||
|
||||
suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
[lints]
|
||||
sketchy-null-number=warn
|
||||
sketchy-null-mixed=warn
|
||||
sketchy-number=warn
|
||||
untyped-type-import=warn
|
||||
nonstrict-import=warn
|
||||
deprecated-type=warn
|
||||
unsafe-getters-setters=warn
|
||||
inexact-spread=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
deprecated-utility=error
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
nonstrict-import
|
||||
sketchy-null
|
||||
unclear-type
|
||||
unsafe-getters-setters
|
||||
untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.105.0
|
||||
^0.78.0
|
||||
|
||||
13
.gitignore
vendored
@@ -4,7 +4,6 @@ build-ios
|
||||
*.zip
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
tmp/
|
||||
|
||||
# OSX
|
||||
#
|
||||
@@ -22,6 +21,7 @@ build/
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
xcshareddata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
@@ -30,8 +30,7 @@ DerivedData
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
.podinstall
|
||||
xcshareddata/
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@@ -39,10 +38,6 @@ ios/Pods
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
android/app/bin
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# node.js
|
||||
#
|
||||
@@ -88,6 +83,10 @@ ios/sentry.properties
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
# Pods
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
|
||||
397
CHANGELOG.md
@@ -1,389 +1,12 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## 1.25.0 Release
|
||||
- Release Date: November 16, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Mattermost monokai theme no longer worked properly on mobile apps.
|
||||
- Fixed an issue on Android where the notification badge count didn't update when using multiple channels.
|
||||
- Fixed an issue on Android where test notifications did not work properly.
|
||||
- Fixed an issue where "In-app" notifications caused the app badge count to get out of sync.
|
||||
- Fixed an issue on Android where email notification setting displayed was not updated when the setting was changed.
|
||||
- Fixed an issue where Favorite channels list didn't update if the app was running in the background.
|
||||
- Fixed an issue where the timezone setting did not update when changing it back to set automatically.
|
||||
- Fixed an issue on iOS where clicking on a hashtag from "recent mentions" (or flagged posts) returned the user to the channel instead of displaying hashtag search results.
|
||||
- Fixed an issue where tapping on a hashtag engaged a keyboard for a moment before displaying search results.
|
||||
- Fixed an issue where posts of the same thread appeared to be from different threads if separated by a new message line.
|
||||
- Fixed styling issues on iOS for Name, Purpose and Header information on the channel info screen.
|
||||
- Fixed styling issues with bot posts timestamps in search results and pinned posts.
|
||||
- Fixed styling issues on single sign-on screen in landscape view on iOS iPhone X and later.
|
||||
- Fixed styling issues on iOS for the Helper text on Settings screens.
|
||||
- Fixed an issue where the thread view header theme was inconsistent during transition back to main channel view.
|
||||
- Fixed an issue on iOS where the navigation bar tucked under the phone's status bar when switching orientation.
|
||||
- Fixed an issue on iOS where the keyboard flashed darker when Automatic Replies had been previously enabled.
|
||||
- Fixed an issue on Android where uploading pictures from storage or camera required unwanted permissions.
|
||||
- Fixed an issue where ``mobile.message_length.message`` did not match webapp's ``create_post.error_message``.
|
||||
|
||||
### Known Issues
|
||||
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
|
||||
## 1.24.0 Release
|
||||
- Release Date: October 16, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Sidebar UI/UX improvements
|
||||
- Improved usability and styling of the channel drawer.
|
||||
|
||||
### Improvements
|
||||
- Added the ability to paste images on input text box.
|
||||
- Added copy and paste protection managed configuration support for Android.
|
||||
- Added a confirmation dialog when posting a message with `@channel` and `@all`.
|
||||
- Added support for safe area in landscape view on iOS.
|
||||
- Changed recent date separators to read Today/Yesterday.
|
||||
- Added an autocomplete to the edit channel screen.
|
||||
- Emoji picker search now ignores the leading colon.
|
||||
- Added support for emoji not requiring a whitespace to render.
|
||||
- Added support for footer and footer_icon in message attachments.
|
||||
- Added a password type for interactive dialogs.
|
||||
- Added support for introductory markdown paragraph in interactive dialogs.
|
||||
- Added support for boolean elements in interactive dialogs.
|
||||
- Improved the permissions prompt if Mattermost doesn't have permission to the photo library.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the notification badge could get out of sync when reading messages in another client.
|
||||
- Fixed an issue where the notification badge number did not reset when opening a push notification.
|
||||
- Fixed an issue where SafeArea insets were not working properly on new iPhone 11 models.
|
||||
- Fixed an issue where long press on a system message in an archived channel locked up the app.
|
||||
- Fixed an issue where tapping on a hashtag while replying to search results didn't open search correctly.
|
||||
- Fixed an issue where the channel list panel was missing for a user when they were added to a new team by another user.
|
||||
- Fixed an issue where once in a thread, pressing a channel link appeared to do nothing.
|
||||
- Fixed an issue where file previews could scroll to the left until all files were out of view.
|
||||
- Fixed an issue on iOS where user was unable to select an emoji from two rows on the bottom of the emoji picker.
|
||||
- Fixed an issue where duplicate pinned posts displayed after editing pinned post from Pinned Posts screen.
|
||||
- Fixed an issue where the reply arrow overlapped a posts's timestamp in some cases.
|
||||
- Fixed an issue where post textbox did not clear after using a slash command.
|
||||
- Fixed an issue where users were are not immediately removed from the mention auto-complete when those users were deactivated.
|
||||
- Fixed an issue where returning to a channel from a thread view could trigger a long-press menu that couldn't be dismissed.
|
||||
- Fixed an issue with a missing "(you)" suffix in the channel header of a self Direct Message.
|
||||
- Fixed an issue where the Connected banner got stuck open after the WebSocket was connected.
|
||||
- Fixed an issue where the text input area in Android Share extension did not use available space.
|
||||
- Fixed an issue where Windows dark theme was not consistent when viewing an archived channel.
|
||||
- Fixed an issue where interactive dialogs rendered out of safe area view on landscape orientation.
|
||||
- Fixed an issue where a themed "Delete Documents & Data" action flashed a white screen.
|
||||
|
||||
### Known Issues
|
||||
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
|
||||
## 1.23.1 Release
|
||||
- Release Date: September 27, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed issues causing the app to crash on some devices.
|
||||
|
||||
## 1.23.0 Release
|
||||
- Release Date: September 16, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where some Giphy actions were not working in ephemeral posts on mobile.
|
||||
- Fixed an issue where users were unable to create new channels when "Combine all channel types" was selected.
|
||||
- Fixed an issue on Android EMM where a crash occurred when tapping **Go to Settings**.
|
||||
- Fixed an issue on iOS where the in-app "Date" localization persisted after server and user changed.
|
||||
- Fixed an issue where the download step was showing when previewing a video right after posting it.
|
||||
- Fixed an issue on Android where cancelling a video download twice in a row showed an error.
|
||||
- Fixed an issue where file attachment thumbnail/preview could fail to load and not be able to be reloaded.
|
||||
- Fixed an issue on Android where **Channel > Add Members > ADD** text changed to black.
|
||||
- Fixed an issue on iOS where the **Cancel** label text didn't fit in one line in German language.
|
||||
- Fixed an issue where longer than allowed reply posts kept showing a warning with every backspace.
|
||||
- Fixed an issue where there was a delay in search box and emoji content width change when switching to/from portrait/landscape view.
|
||||
- Fixed an issue where deactivated users did not appear in the "Jump to..." screen.
|
||||
- Fixed an issue where "@undefined has joined the channel" was shown instead of "Someone has joined the channel" when a user joined a channel that another user was viewing.
|
||||
- Fixed an issue on Android where the reply arrow was cut off in search results.
|
||||
- Fixed an issue where changing display theme from webapp didn't work properly on mobile.
|
||||
- Fixed an issue on iOS where a bot account icon style was broken.
|
||||
- Fixed an issue with an incorrect UI text for location of touch ID setting.
|
||||
|
||||
### Known Issues
|
||||
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
- When users are deactivated, they are not immediately removed from the mention auto-complete. [MM-17953](https://mattermost.atlassian.net/browse/MM-17953)
|
||||
|
||||
## 1.22.1 Release
|
||||
- Release Date: August 23, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the apps crashed when setting the language to Chinese Traditional.
|
||||
- Fixed an issue on Android where push notification receipt delivery failed due to invalid server URL.
|
||||
- Fixed an issue where the apps crashed when launched via a notification.
|
||||
- Fixed an issue where posts made while the app was closed did not appear until refresh.
|
||||
|
||||
## 1.22.0 Release
|
||||
- Release Date: August 16, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Support for iOS13 and Android Q
|
||||
- Added support for iOS13 and Android Q which are to be released later this year.
|
||||
|
||||
### Improvements
|
||||
- Added support for Interactive Dialog with no elements.
|
||||
- Added a setting for tablets to enable or disable fixed sidebar.
|
||||
- Changed "about" section references to use the site name when it is configured in **System Console > Custom Branding > Site Name**.
|
||||
- Added support for plus-sign and period/dot in custom URL schemes.
|
||||
- Added "Edit profile" button to right-hand side menu and to users' own profile pop-over.
|
||||
- Message draft is now saved when closing the app.
|
||||
- Removing a link preview on webapp now also removes it on the mobile app.
|
||||
- Added ability to select and copy channel header text and purpose.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed a few mobile app crash / fatal error issues.
|
||||
- Fixed an issue where timestamps were off on Android.
|
||||
- Fixed an issue where contents of ephemeral posts from /giphy were not being displayed on mobile.
|
||||
- Fixed an issue where team/channel page dots at the bottom of left-hand side overlapped with the last Direct Message channel.
|
||||
- Fixed an issue where network reconnection incorrectly showed refreshing messages failed.
|
||||
- Fixed an issue with the channel sidebar theme colors not being respected on iPhone X.
|
||||
- Fixed an issue where "Message failed to send" had incorrect app badge behaviour.
|
||||
- Fixed an issue where a white screen was briefly shown after pressing "Send Message" when viewing a user's profile.
|
||||
- Fixed an issue on Android where using "Https" instead of "https" in the url of an image didn't show the preview.
|
||||
- Fixed an issue where the client ``setCSRFFromCookie`` did not look for subpaths when accessing cookies.
|
||||
- Fixed an issue where archived teams reappeared in selector.
|
||||
- Fixed an issue where users' profile picture and name did not get updated after websocket disconnect.
|
||||
|
||||
### Known Issues
|
||||
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
- Some Giphy actions do not work in ephemeral posts. [MM-17842](https://mattermost.atlassian.net/browse/MM-17842)
|
||||
|
||||
## 1.21.2 Release
|
||||
- Release Date: August 1, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the mobile apps logged out without a session expiry notification.
|
||||
|
||||
## 1.21.1 Release
|
||||
- Release Date: July 22, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue on Android where logging in using SSO failed when the Mattermost server was running on a subpath.
|
||||
|
||||
## 1.21.0 Release
|
||||
- Release Date: July 16, 2019
|
||||
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed a few mobile app crash / fatal error issues.
|
||||
- Fixed an issue where having the sidebar open at all times on tablets did not work for split view.
|
||||
- Fixed an issue where new messages were often hidden behind a keyboard or text field.
|
||||
- Fixed an issue on Android where channel sorting didn't match the web app.
|
||||
- Fixed an issue where sharing a GIF via keyboard resulted in an error screen.
|
||||
- Fixed an issue where long-press menu could not be dragged up when rotating the device to landscape view while the menu was open.
|
||||
- Fixed an issue on Android where push notification settings were only saved after closing the settings page.
|
||||
- Fixed an issue where users on View Members list had an icon that appeared to be selectable but was not.
|
||||
- Fixed an issue where "Jump To" showed archived channels the user did not belong to instead of the ones the user was a member of.
|
||||
- Fixed an issue where changing the timezone setting manually to "Set automatically" did not work on the mobile app.
|
||||
- Fixed an issue where setting a position field for AD/LDAP sync or SAML in the System Console did not block the user from changing it in account settings.
|
||||
- Fixed an issue where **Channel Info > Manage/View Members** screen didn't load channel users.
|
||||
- Fixed an issue where enabling large fonts on iOS caused the left-hand side text to be cut off.
|
||||
- Fixed an issue on Android where users could not reply to a push notification if the mention was in a thread message.
|
||||
|
||||
### Known Issues
|
||||
- (Android) On subpath server, logging in using GitLab or OneLogin fails to display Mattermost. [MM-16829](https://mattermost.atlassian.net/browse/MM-16829)
|
||||
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
|
||||
- Android apps slow down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
|
||||
## 1.20.2 Release
|
||||
- Release Date: July 10, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Moto G7 devices were detected as tablets and showed a fixed width sidebar.
|
||||
- Fixed an issue where having the sidebar open at all times on tablets did not work on split view.
|
||||
|
||||
## 1.20.1 Release
|
||||
- Release Date: June 21, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where some Android devices were crashing.
|
||||
- Fixed an issue where messages were missing after reconnecting the network.
|
||||
|
||||
## 1.20.0 Release
|
||||
- Release Date: June 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Tablet Improvements
|
||||
- Channel sidebar now remains open at a fixed width on tablet devices.
|
||||
|
||||
#### iOS Keyboard Dismissal
|
||||
- If the keyboard is open, swiping down past it now closes it.
|
||||
|
||||
#### Profile Telemetry for Android Beta Builds
|
||||
- To improve Android app performance, we are collecting trace events and device information, collectively known as metrics, to identify slow performing key areas. Those metrics will be sent only from users using Android app beta build starting in version v1.20, who are logged in to servers that allow sending [diagnostic information](https://docs.mattermost.com/administration/config-settings.html#enable-diagnostics-and-error-reporting).
|
||||
|
||||
### Improvements
|
||||
- Increased the double tap delay for post action buttons.
|
||||
- Implemented assets for Adaptive icons.
|
||||
- Users are now brought to the bottom of the channel when posting a message.
|
||||
- Users can now execute actions while the keyboard is open.
|
||||
- Added support on iOS for IPv6 on LTE networks.
|
||||
- Added support for LDAP Group constrained feature with v5.12 servers.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where a post wasn't immediately removed when deleting another user's post.
|
||||
- Fixed an issue where the cursor jumped back when typing after auto-completing a slash command.
|
||||
- Fixed an issue where the iOS app didn’t properly restore its connection after disconnect.
|
||||
- Fixed an issue where the long press menu persisted after returning from a thread.
|
||||
- Fixed an issue on Android where the "Write to [channel name]" was cut off for group messages with several users.
|
||||
- Fixed an issue where users were not able to flag or unflag posts in a read-only channel.
|
||||
- Fixed an issue where the progress indicator was negative while downloading a video.
|
||||
- Fixed an issue where the edit post modal didn’t have an autocorrect.
|
||||
- Fixed an issue where the 'I forgot my password' option was available on the mobile client even with Email Authentication disabled on the server.
|
||||
- Fixed an issue with large separation between placeholders on iPad when a channel was loading.
|
||||
- Fixed an issue where "Show More" was not removed after the post was edited to a single line.
|
||||
|
||||
### Known Issues
|
||||
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
|
||||
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
|
||||
|
||||
## 1.19.0 Release
|
||||
- Release Date: May 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Android managed config was lost on the thread view.
|
||||
- Fixed an issue where contents of ephemeral posts did not display on the mobile app.
|
||||
- Fixed a few mobile app crash / fatal error issues.
|
||||
- Fixed an issue with an expanding animation when tapping on Jump to Channel in the channel list.
|
||||
- Fixed an issue on iOS where animated custom emoji weren't animated.
|
||||
- Fixed an issue on iOS where users were unable to create channel name of 2 characters.
|
||||
- Fixed an issue on iOS where emoji appeared too close, with uneven spacing, and too small in the info modal.
|
||||
- Added an error handler when sharing text that was over server's maximum post size with the iOS Share Extension.
|
||||
- Fixed an issue where users could upload a GIF as a profile image.
|
||||
|
||||
### Known Issues
|
||||
- Buttons inside ephemeral posts are not clickable / functional on the mobile app.
|
||||
|
||||
## 1.18.1 Release
|
||||
- Release Date: April 18, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed a crash issue caused by a malformed post textbox localize string.
|
||||
- Fixed an issue where iOS crashed when trying to log in using SSO and the SSO provider set a cookie without an expiration date.
|
||||
|
||||
## 1.18.0 Release
|
||||
- Release Date: April 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
- ``Bot`` tags were added for bot accounts feature in server v5.10 and mobile v1.18, meaning that mobile v1.17 and earlier don't support the tags.
|
||||
|
||||
### Highlights
|
||||
- Added support for Office365 single sign-on (SSO).
|
||||
- Added support for Integrated Windows Authentication (IWA).
|
||||
|
||||
### Improvements
|
||||
- Added the ability for channel links to open inside the app.
|
||||
- Added ability for emojis and hyperlinks to render in the message attachment title.
|
||||
- Added Chinese support for words that trigger mentions.
|
||||
- Added a setting to the system console to change the minimum length of hashtags.
|
||||
- Added a reply option to long press context menu.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where blank spaces broke markdown tables.
|
||||
- Fixed an issue where deactivated users appeared on "Add Members" modal but not on the search results.
|
||||
- Fixed an issue on Android where extra text in the search box appeared after using the autocomplete drop-down.
|
||||
- Fixed an issue with multiple text entries when typing with Shift+Letter on Android.
|
||||
- Fixed an issue where push notifications badges did not always clear when read on another device.
|
||||
- Fixed an issue where opening a single or group notification did not take the user into the channel where the notification came from.
|
||||
- Fixed an issue where timezone did not automatically update on Android when travelling to another timezone.
|
||||
- Fixed an issue where the user mention autocomplete drop-down was case sensitive.
|
||||
- Fixed an issue where system admininistrators were able to see the full long press menu when long pressing a system message.
|
||||
- Fixed an issue where users were not able to unflag posts from "Flagged Posts" when opened from a read-only channel.
|
||||
- Fixed an issue where users were unable to create channel names of 2 byte characters.
|
||||
|
||||
### Known Issues
|
||||
- Content for ephemeral messages is not displayed on Mattermost Mobile Apps.
|
||||
|
||||
## 1.17.0 Release
|
||||
- Release Date: March 20, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
|
||||
- If you are using an EMM provider via AppConfig, make sure to add two new settings, `useVPN` and `timeoutVPN`, to your AppConfig file. The settings were added for EMM connections using VPN on-demand - one to indicate if every request should wait for the VPN connection to be established, and another to set the timeout in seconds. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
|
||||
- Fixed support for EMM connections using VPN on-demand to indicate that every request should wait for the VPN connection to be establish and to set the value in seconds for the timeout. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
@@ -409,7 +32,7 @@
|
||||
- Release Date: February 21, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
@@ -421,7 +44,7 @@
|
||||
- Release Date: February 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
@@ -448,7 +71,7 @@
|
||||
- Release Date: January 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
@@ -468,7 +91,7 @@
|
||||
- Release Date: December 28, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
@@ -480,7 +103,7 @@
|
||||
- Release Date: December 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Compatibility
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
@@ -515,7 +138,7 @@
|
||||
- Release Date: November 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
**Compatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the Android app did not allow establishing a network connection with any server that used a self-signed certificate that had the CA certificate user installed on the device.
|
||||
@@ -537,7 +160,7 @@
|
||||
- Release Date: October 18, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
**Compatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue preventing some users from authenticating using OKTA
|
||||
@@ -546,7 +169,7 @@
|
||||
- Release Date: October 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
**Compatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Highlights
|
||||
|
||||
|
||||
@@ -2,4 +2,33 @@
|
||||
|
||||
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
|
||||
|
||||
When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/).
|
||||
### Review Process for this Repo
|
||||
|
||||
After following the steps in the [Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html), submitted pull requests go through the review process outlined below. We aim to start reviewing pull requests in this repo the week they are submitted, but the length of time to complete the process will vary depending on the pull request.
|
||||
|
||||
The one exception may be around release time, where the review process may take longer as the team focuses on our [release process](https://docs.mattermost.com/process/release-process.html).
|
||||
|
||||
#### `Stage 1: PM Review`
|
||||
|
||||
A Product Manager will review the pull request to make sure it:
|
||||
|
||||
1. Fits with our product roadmap
|
||||
2. Works as expected
|
||||
3. Meets UX guidelines
|
||||
|
||||
This step is sometimes skipped for bugs or small improvements with a ticket, but always happens for new features or pull requests without a related ticket.
|
||||
|
||||
The Product Manager may come back with some bugs or UI improvements to fix before the pull request moves on to the next stage.
|
||||
|
||||
#### `Stage 2: Dev Review`
|
||||
|
||||
Two developers will review the pull request and either give feedback or `+1` the PR.
|
||||
|
||||
Any comments will need to be addressed before the pull request moves on to the last stage.
|
||||
|
||||
- PRs that do not follow Style Guides cannot be merged
|
||||
|
||||
#### `Stage 3: Ready to Merge`
|
||||
|
||||
The review process is complete, and the pull request will be merged.
|
||||
|
||||
|
||||
54
Makefile
@@ -1,6 +1,5 @@
|
||||
.PHONY: pre-run pre-build clean
|
||||
.PHONY: check-style
|
||||
.PHONY: i18n-extract-ci
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
|
||||
@@ -64,18 +63,25 @@ check-style: node_modules ## Runs eslint
|
||||
clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleaning started
|
||||
|
||||
@rm -f .podinstall
|
||||
@rm -rf ios/Pods
|
||||
@rm -rf node_modules
|
||||
@rm -f .podinstall
|
||||
@rm -rf dist
|
||||
@rm -rf ios/build
|
||||
@rm -rf ios/Pods
|
||||
@rm -rf android/app/build
|
||||
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@./node_modules/.bin/patch-package
|
||||
@./node_modules/.bin/jetify
|
||||
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
|
||||
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
|
||||
|
||||
@# Need to copy custom RNCWebViewManager.java and RNCWEKWebView.m that implements IWA support for the WebView to avoid forking the lib
|
||||
@cp ./native_modules/RNCWebViewManager.java node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
|
||||
@cp ./native_modules/RNCWKWebView.m node_modules/react-native-webview/ios/RNCWKWebView.m
|
||||
|
||||
# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
|
||||
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
|
||||
|
||||
@rm -f node_modules/intl/.babelrc
|
||||
@# Hack to get react-intl and its dependencies to work with react-native
|
||||
@@ -84,6 +90,12 @@ post-install:
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
|
||||
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
|
||||
@sed -i'' -e "s|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java
|
||||
@sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle
|
||||
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
|
||||
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
|
||||
fi
|
||||
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
$(call start_packager)
|
||||
@@ -149,7 +161,7 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
@@ -164,20 +176,20 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
|
||||
fi; \
|
||||
fi
|
||||
|
||||
build: | stop pre-build check-style i18n-extract-ci ## Builds the app for Android & iOS
|
||||
build: | stop pre-build check-style ## Builds the app for Android & iOS
|
||||
$(call start_packager)
|
||||
@echo "Building App"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
|
||||
$(call stop_packager)
|
||||
|
||||
|
||||
build-ios: | stop pre-build check-style i18n-extract-ci ## Builds the iOS app
|
||||
build-ios: | stop pre-build check-style ## Builds the iOS app
|
||||
$(call start_packager)
|
||||
@echo "Building iOS app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
$(call stop_packager)
|
||||
|
||||
build-android: | stop pre-build check-style i18n-extract-ci prepare-android-build ## Build the Android app
|
||||
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
|
||||
$(call start_packager)
|
||||
@echo "Building Android app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
@@ -185,7 +197,13 @@ build-android: | stop pre-build check-style i18n-extract-ci prepare-android-buil
|
||||
|
||||
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
|
||||
$(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 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/
|
||||
$(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
|
||||
@@ -197,16 +215,19 @@ ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version o
|
||||
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
|
||||
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
|
||||
@rm -rf build-ios/
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-simulator-x86_64.app.zip os_type:iOS
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
|
||||
$(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
|
||||
$(call stop_packager)
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
|
||||
build-pr: | can-build-pr stop pre-build check-style i18n-extract-ci ## Build a PR from the mattermost-mobile repo
|
||||
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
|
||||
$(call start_packager)
|
||||
@echo "Building App from PR ${PR_ID}"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
|
||||
@@ -219,16 +240,9 @@ can-build-pr:
|
||||
fi
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
npm run mmjstool -- i18n extract-mobile
|
||||
@[[ -d $(MM_UTILITIES_DIR) ]] || echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||
@[[ -d $(MM_UTILITIES_DIR) ]] && cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-mobile
|
||||
|
||||
i18n-extract-ci:
|
||||
mkdir -p tmp
|
||||
cp assets/base/i18n/en.json tmp/en.json
|
||||
mkdir -p tmp/fake-webapp-dir/i18n/
|
||||
echo '{}' > tmp/fake-webapp-dir/i18n/en.json
|
||||
npm run mmjstool -- i18n extract-mobile --webapp-dir tmp/fake-webapp-dir --mobile-dir .
|
||||
diff tmp/en.json assets/base/i18n/en.json
|
||||
rm -rf tmp
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
|
||||
578
NOTICE.txt
@@ -7,6 +7,42 @@ NOTICES:
|
||||
This document includes a list of open source components used in Mattermost Mobile, including those that have been modified.
|
||||
|
||||
--------
|
||||
## @babel/polyfill
|
||||
|
||||
This product contains 'polyfill' by Sebastian McKenzie.
|
||||
|
||||
Provides polyfills necessary for a full ES2015+ environment
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://babeljs.io/
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014-present Sebastian McKenzie and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @babel/runtime
|
||||
|
||||
This product contains 'runtime' by Sebastian McKenzie.
|
||||
@@ -43,146 +79,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/async-storage
|
||||
|
||||
This product contains 'async-storage' by Krzysztof Borowy.
|
||||
|
||||
Asynchronous, persistent, key-value storage system for React Native.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/react-native-async-storage#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/cameraroll
|
||||
|
||||
This product contains 'cameraroll' by Bartol Karuza.
|
||||
|
||||
React-native native module that provides access to the local camera roll or photo library
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/react-native-cameraroll
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/netinfo
|
||||
|
||||
This product contains 'netinfo' by Matt Oakes.
|
||||
|
||||
React Native Network Info API for iOS & Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-community/react-native-netinfo#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @sentry/react-native
|
||||
|
||||
This product contains 'react-native-sentry' by Sentry.
|
||||
|
||||
Official Sentry SDK for react-native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/getsentry/react-native-sentry
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Sentry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## analytics-react-native
|
||||
|
||||
This product contains 'analytics-react-native' by Javier Alvarez.
|
||||
@@ -382,37 +278,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## core-js
|
||||
|
||||
This product contains 'core-js' by Denis Pushkarev.
|
||||
|
||||
Modular standard library for JavaScript.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/zloirock/core-js
|
||||
|
||||
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## deep-equal
|
||||
|
||||
This product contains 'deep-equal' by James Halliday.
|
||||
@@ -445,39 +310,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## deepmerge
|
||||
|
||||
This product contains 'deepmerge' by Josh Duff.
|
||||
|
||||
A library for deep (recursive) merging of Javascript objects.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/TehShrike/deepmerge
|
||||
|
||||
* LICENSE: The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012 James Halliday, Josh Duff, and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## emoji-regex
|
||||
|
||||
This product contains 'emoji-regex' by Mathias Bynens.
|
||||
@@ -514,39 +346,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## form-data
|
||||
|
||||
This product contains 'form-data' by form-data.
|
||||
|
||||
A module to create readable "multipart/form-data" streams. Can be used to submit forms and file uploads to other web applications.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/form-data/form-data
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## fuse.js
|
||||
|
||||
This product contains 'fuse.js' by Kirollos Risk.
|
||||
@@ -926,7 +725,7 @@ Common code (API client, Redux stores, logic, utility functions) for building a
|
||||
|
||||
* LICENSE: Apache-2.0
|
||||
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
@@ -1348,19 +1147,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-android-open-settings
|
||||
|
||||
This product contains 'react-native-android-open-settings' by Leonardo Velasquez.
|
||||
|
||||
Open Android settings from your React Native app
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/levelasquez/react-native-android-open-settings
|
||||
|
||||
* LICENSE: ISC
|
||||
|
||||
---
|
||||
|
||||
## react-native-animatable
|
||||
|
||||
This product contains 'react-native-animatable' by Joel Arvidsson.
|
||||
@@ -1396,6 +1182,41 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-bottom-sheet
|
||||
|
||||
This product contains 'react-native-bottom-sheet' by WhatAKitty.
|
||||
|
||||
React Native Bottom sheet for android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/WhatAKitty/react-native-bottom-sheet#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 WhatAKitty
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-button
|
||||
|
||||
This product contains 'react-native-button' by James Ide.
|
||||
@@ -1580,7 +1401,7 @@ SOFTWARE.
|
||||
|
||||
This product contains 'react-native-doc-viewer' by Philipp Hecht.
|
||||
|
||||
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
|
||||
React Native Native Module Bridge Quicklock Document Viewer for IOS + Android supports pdf, png, jpg, xls, ppt, doc, docx, pptx, xlx + Video Player mp4 supported
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/philipphecht/react-native-doc-viewer/blob/master/README.md
|
||||
@@ -1681,41 +1502,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-fast-image
|
||||
|
||||
This product contains 'react-native-fast-image' by Dylan Vann.
|
||||
|
||||
Performant React Native image component
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/DylanVann/react-native-fast-image
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Dylan Vann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-gesture-handler
|
||||
|
||||
This product contains 'react-native-gesture-handler' by Krzysztof Magiera.
|
||||
@@ -1751,41 +1537,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-haptic-feedback
|
||||
|
||||
This product contains 'react-native-haptic-feedback' by Milk and Cookies.
|
||||
|
||||
React-Native Haptic Feedback for iOS with a similar behaviour for Android
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/milk-and-cookies-io/react-native-haptic-feedback
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Michael Kuczera
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-image-gallery
|
||||
|
||||
This product contains a modified version of 'react-native-image-gallery' by Archriss.
|
||||
@@ -1880,41 +1631,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-keyboard-tracking-view
|
||||
|
||||
This product contains a modified version of 'react-native-keyboard-tracking-view' by Artal Druk.
|
||||
|
||||
React Native UI component which tracks the keyboard
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/wix/react-native-keyboard-tracking-view
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Wix.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-keychain
|
||||
|
||||
This product contains 'react-native-keychain' by Joel Arvidsson.
|
||||
@@ -2006,7 +1722,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
||||
|
||||
## react-native-navigation
|
||||
|
||||
This product contains a modified version of 'react-native-navigation' by Wix.com.
|
||||
This product contains a modified version of 'react-native-navigation' by Daniel Zlotin.
|
||||
|
||||
React Native Navigation - truly native navigation for iOS and Android
|
||||
|
||||
@@ -2077,7 +1793,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
This product contains 'react-native-passcode-status' by Mark Vayngrib.
|
||||
|
||||
check if device-level passcode is supported/enabled/disabled
|
||||
check passcode status on device
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/tradle/react-native-passcode-status
|
||||
@@ -2143,6 +1859,41 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-recyclerview-list
|
||||
|
||||
This product contains 'react-native-recyclerview-list' by GitHub user "godness84".
|
||||
|
||||
A RecyclerView implementation for React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/godness84/react-native-recyclerview-list#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Marc Shilling
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-safe-area
|
||||
|
||||
This product contains 'react-native-safe-area' by Masayuki Iwai.
|
||||
@@ -2191,20 +1942,55 @@ This package simplifies constructing the getItemLayout prop for react native Sec
|
||||
|
||||
Copyright (c) 2017 Jan Soendermann
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
## react-native-sentry
|
||||
|
||||
This product contains 'react-native-sentry' by Sentry.
|
||||
|
||||
Official Sentry SDK for react-native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/getsentry/react-native-sentry
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Sentry
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-slider
|
||||
|
||||
This product contains 'react-native-slider' by Jean Regisser.
|
||||
@@ -2372,7 +2158,7 @@ SOFTWARE.
|
||||
|
||||
## react-native-webview
|
||||
|
||||
This product contains a modified version of 'react-native-webview' by Jamon Holmgren.
|
||||
This product contains 'react-native-webview' by Jamon Holmgren.
|
||||
|
||||
React Native WebView component for iOS, Android, and Windows 10 (coming soon)
|
||||
|
||||
@@ -2480,29 +2266,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---
|
||||
|
||||
## react-navigation-stack
|
||||
|
||||
This product contains 'react-navigation-stack' by react-navigation.
|
||||
|
||||
Stack navigator for React Navigation
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-navigation/stack
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 React Native Community
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
----
|
||||
|
||||
## react-redux
|
||||
|
||||
This product contains 'react-redux' by Dan Abramov.
|
||||
@@ -2608,41 +2371,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## redux-offline
|
||||
|
||||
This product contains 'redux-offline' by redux-offline.
|
||||
|
||||
Build Offline-First Apps for Web and React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/redux-offline/redux-offline
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Jani Eväkallio
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## redux-persist
|
||||
|
||||
This product contains 'redux-persist' by Zack Story.
|
||||
@@ -2829,7 +2557,7 @@ Display some placeholder stuff before rendering your text or media content in Re
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
Copyright (c) 2004-Today Marvin Frachet
|
||||
Copyright (c) 2004-2018 Marvin Frachet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
|
||||
Please make sure you've read the [pull request](https://developers.mattermost.com/contribute/getting-started/contribution-checklist/) section of our [code contribution guidelines](https://developers.mattermost.com/contribute/getting-started/).
|
||||
|
||||
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
|
||||
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
|
||||
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
|
||||
-->
|
||||
When filling in a section please remove the help text and the above text.
|
||||
|
||||
#### Summary
|
||||
<!--
|
||||
A brief description of what this pull request does.
|
||||
-->
|
||||
[A brief description of what this pull request does.]
|
||||
|
||||
#### Ticket Link
|
||||
<!--
|
||||
If this pull request addresses a Help Wanted ticket, please link the relevant GitHub issue, e.g.
|
||||
|
||||
Fixes https://github.com/mattermost/mattermost-server/issues/XXXXX
|
||||
|
||||
Otherwise, link the JIRA ticket.
|
||||
-->
|
||||
[Please link the GitHub issue or Jira ticket this PR addresses.]
|
||||
|
||||
#### Checklist
|
||||
<!--
|
||||
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
|
||||
-->
|
||||
[Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.]
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] All new/modified APIs include changes to [mattermost-redux](https://github.com/mattermost/mattermost-redux) (please link)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
This PR was tested on: [Device name(s), OS version(s)]
|
||||
|
||||
#### Screenshots
|
||||
<!--
|
||||
If the PR includes UI changes, include screenshots/GIFs (for both iOS and Android if possible).
|
||||
-->
|
||||
[If the PR includes UI changes, include screenshots (for both iOS and Android if possible).]
|
||||
|
||||
@@ -20,7 +20,7 @@ To help with testing app updates before they're released, you can:
|
||||
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P)
|
||||
- [iOS](https://mattermost-fastlane.herokuapp.com/)
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
- Device information
|
||||
@@ -29,14 +29,10 @@ To help with testing app updates before they're released, you can:
|
||||
- Expected behavior
|
||||
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
|
||||
You can leave the Beta testing program at any time:
|
||||
- On Android, [click this link](https://play.google.com/apps/testing/com.mattermost.rnbeta) while logged in with your Google Play email address used to opt-in for the Beta program, then click **Leave the program**.
|
||||
- On iOS, access the `Mattermost Beta` app page in TestFlight and click **Stop Testing**.
|
||||
|
||||
### Contribute Code
|
||||
|
||||
1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) for issues marked as [Help Wanted]
|
||||
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted]
|
||||
2. Comment to let people know you’re working on it
|
||||
3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment
|
||||
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
|
||||
@@ -74,8 +74,8 @@ import com.android.build.OutputFile
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js",
|
||||
bundleConfig: "metro.config.js",
|
||||
enableHermes: false,
|
||||
bundleCommand: "ram-bundle",
|
||||
bundleConfig: "packager-config.js"
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
@@ -87,7 +87,7 @@ if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
flavorAware: false
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
|
||||
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,46 +105,24 @@ def enableSeparateBuildPerCPUArchitecture = false
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore.
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc-intl:r241213'
|
||||
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
*
|
||||
* This should be set on project.ext.react and mirrored here. If it is not set
|
||||
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
|
||||
* and the benefits of using Hermes will therefore be sharply reduced.
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
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
|
||||
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative60"
|
||||
versionCode 258
|
||||
versionName "1.26.2"
|
||||
versionCode 193
|
||||
versionName "1.19.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -163,7 +141,7 @@ android {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
include "armeabi-v7a", "x86"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
@@ -179,7 +157,7 @@ android {
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
signingConfig null
|
||||
matchingFallbacks = ['release']
|
||||
matchingFallbacks = ['debug', 'release']
|
||||
}
|
||||
}
|
||||
// applicationVariants are e.g. debug, release
|
||||
@@ -187,7 +165,7 @@ android {
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
@@ -211,6 +189,9 @@ repositories {
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'android-jsc') {
|
||||
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
|
||||
}
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
@@ -228,27 +209,19 @@ configurations.all {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
if (enableHermes) {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation 'com.android.support:percent:28.0.0'
|
||||
implementation "com.google.firebase:firebase-messaging:17.3.0"
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation project(':react-native-document-picker')
|
||||
implementation project(':react-native-keychain')
|
||||
implementation project(':react-native-doc-viewer')
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-navigation')
|
||||
implementation project(':react-native-image-picker')
|
||||
implementation project(':react-native-bottom-sheet')
|
||||
implementation project(':react-native-device-info')
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation project(':react-native-cookies')
|
||||
@@ -258,23 +231,19 @@ dependencies {
|
||||
implementation project(':react-native-local-auth')
|
||||
implementation project(':jail-monkey')
|
||||
implementation project(':react-native-youtube')
|
||||
implementation project(':react-native-sentry')
|
||||
implementation project(':react-native-exception-handler')
|
||||
implementation project(':rn-fetch-blob')
|
||||
implementation project(':react-native-recyclerview-list')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-gesture-handler')
|
||||
implementation project(':@react-native-community_async-storage')
|
||||
implementation project(':@react-native-community_netinfo')
|
||||
implementation project(':@sentry_react-native')
|
||||
implementation project(':react-native-android-open-settings')
|
||||
implementation project(':react-native-haptic-feedback')
|
||||
|
||||
implementation project(':react-native-fast-image')
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:2.0.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:2.0.0'
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:1.10.0'
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-webp:2.0.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:2.0.0'
|
||||
implementation 'com.facebook.fresco:animated-webp:1.10.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.10.0'
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -57,7 +57,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -88,7 +88,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -101,4 +101,4 @@
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
7
android/app/proguard-rules.pro
vendored
@@ -8,3 +8,10 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
||||
@@ -2,8 +2,8 @@
|
||||
package="com.mattermost.rnbeta">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission-sdk-23 android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -15,12 +15,9 @@
|
||||
android:allowBackup="false"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme"
|
||||
android:installLocation="auto"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
@@ -29,8 +26,7 @@
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleInstance">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -46,15 +42,13 @@
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:resizeableActivity="true"/>
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
|
||||
<activity
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme"
|
||||
android:taskAffinity="com.mattermost.share">
|
||||
android:theme="@style/AppTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Person;
|
||||
import android.app.Person.Builder;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
@@ -20,27 +18,23 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings.System;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
|
||||
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
|
||||
@@ -48,15 +42,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
|
||||
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
|
||||
|
||||
private static final String PUSH_TYPE_MESSAGE = "message";
|
||||
private static final String PUSH_TYPE_CLEAR = "clear";
|
||||
private static final String PUSH_TYPE_UPDATE_BADGE = "update_badge";
|
||||
|
||||
private NotificationChannel mHighImportanceChannel;
|
||||
private NotificationChannel mMinImportanceChannel;
|
||||
|
||||
private static Map<String, Integer> channelIdToNotificationCount = new HashMap<String, Integer>();
|
||||
private static Map<String, List<Bundle>> channelIdToNotification = new HashMap<String, List<Bundle>>();
|
||||
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
|
||||
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
|
||||
private static AppLifecycleFacade lifecycleFacade;
|
||||
private static Context context;
|
||||
private static int badgeCount = 0;
|
||||
@@ -64,14 +51,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
this.context = context;
|
||||
createNotificationChannels();
|
||||
}
|
||||
|
||||
public static void clearNotification(Context mContext, int notificationId, String channelId) {
|
||||
if (notificationId != -1) {
|
||||
Integer count = channelIdToNotificationCount.get(channelId);
|
||||
if (count == null) {
|
||||
count = -1;
|
||||
Object objCount = channelIdToNotificationCount.get(channelId);
|
||||
Integer count = -1;
|
||||
|
||||
if (objCount != null) {
|
||||
count = (Integer)objCount;
|
||||
}
|
||||
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
@@ -84,6 +72,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
if (count != -1) {
|
||||
int total = CustomPushNotification.badgeCount - count;
|
||||
int badgeCount = total < 0 ? 0 : total;
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), badgeCount);
|
||||
CustomPushNotification.badgeCount = badgeCount;
|
||||
}
|
||||
}
|
||||
@@ -96,78 +85,47 @@ public class CustomPushNotification extends PushNotification {
|
||||
if (mContext != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancelAll();
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceived() throws InvalidNotificationException {
|
||||
final Bundle initialData = mNotificationProps.asBundle();
|
||||
final String type = initialData.getString("type");
|
||||
final String ackId = initialData.getString("ack_id");
|
||||
final String postId = initialData.getString("post_id");
|
||||
final String channelId = initialData.getString("channel_id");
|
||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null ? initialData.getString("id_loaded").equals("true") : false;
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String type = data.getString("type");
|
||||
int notificationId = MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (ackId != null) {
|
||||
notificationReceiptDelivery(ackId, postId, type, isIdLoaded, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (isIdLoaded) {
|
||||
Bundle response = (Bundle) value;
|
||||
mNotificationProps = createProps(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message) {
|
||||
Log.e("ReactNative", code + ": " + message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// notificationReceiptDelivery can override mNotificationProps
|
||||
// so we fetch the bundle again
|
||||
final Bundle data = mNotificationProps.asBundle();
|
||||
|
||||
if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
|
||||
synchronized (channelIdToNotificationCount) {
|
||||
Integer count = channelIdToNotificationCount.get(channelId);
|
||||
if (count == null) {
|
||||
count = 0;
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
channelIdToNotificationCount.put(channelId, count);
|
||||
Object objCount = channelIdToNotificationCount.get(channelId);
|
||||
Integer count = 1;
|
||||
if (objCount != null) {
|
||||
count = (Integer)objCount + 1;
|
||||
}
|
||||
channelIdToNotificationCount.put(channelId, count);
|
||||
|
||||
synchronized (channelIdToNotification) {
|
||||
List<Bundle> list = channelIdToNotification.get(channelId);
|
||||
if (list == null) {
|
||||
list = Collections.synchronizedList(new ArrayList(0));
|
||||
}
|
||||
|
||||
if (PUSH_TYPE_MESSAGE.equals(type)) {
|
||||
String senderName = getSenderName(data);
|
||||
Object bundleArray = channelIdToNotification.get(channelId);
|
||||
List list = null;
|
||||
if (bundleArray == null) {
|
||||
list = Collections.synchronizedList(new ArrayList(0));
|
||||
} else {
|
||||
list = Collections.synchronizedList((List)bundleArray);
|
||||
}
|
||||
synchronized (list) {
|
||||
if (!"clear".equals(type)) {
|
||||
String senderName = getSenderName(data.getString("channel_name"), data.getString("message"));
|
||||
data.putLong("time", new Date().getTime());
|
||||
data.putString("sender_name", senderName);
|
||||
data.putString("sender_id", data.getString("sender_id"));
|
||||
}
|
||||
list.add(0, data);
|
||||
channelIdToNotification.put(channelId, list);
|
||||
}
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
super.postNotification(notificationId);
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
if ("clear".equals(type)) {
|
||||
cancelNotification(data, notificationId);
|
||||
break;
|
||||
} else {
|
||||
super.postNotification(notificationId);
|
||||
}
|
||||
|
||||
notifyReceivedToJS();
|
||||
@@ -182,139 +140,47 @@ public class CustomPushNotification extends PushNotification {
|
||||
digestNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postNotification(int id, Notification notification) {
|
||||
boolean force = false;
|
||||
Bundle bundle = notification.extras;
|
||||
if (bundle != null) {
|
||||
force = bundle.getBoolean("localTest");
|
||||
}
|
||||
|
||||
if (!mAppLifecycleFacade.isAppVisible() || force) {
|
||||
super.postNotification(id, notification);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
|
||||
|
||||
// First, get a builder initialized with defaults from the core class.
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
|
||||
// If Android Oreo or above we need to register a channel
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
String CHANNEL_ID = "channel_01";
|
||||
String CHANNEL_NAME = "Mattermost notifications";
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.setShowBadge(true);
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
notification.setChannelId(CHANNEL_ID);
|
||||
}
|
||||
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(notification, bundle);
|
||||
setNotificationMessagingStyle(notification, bundle);
|
||||
setNotificationChannel(notification, bundle);
|
||||
setNotificationBadgeIconType(notification);
|
||||
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
|
||||
setNotificationSound(notification, notificationPreferences);
|
||||
setNotificationVibrate(notification, notificationPreferences);
|
||||
setNotificationBlink(notification, notificationPreferences);
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
setNotificationNumber(notification, channelId);
|
||||
setNotificationDeleteIntent(notification, notificationId);
|
||||
addNotificationReplyAction(notification, notificationId, bundle);
|
||||
|
||||
notification
|
||||
.setContentIntent(intent)
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void addNotificationExtras(Notification.Builder notification, Bundle bundle) {
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle == null) {
|
||||
userInfoBundle = new Bundle();
|
||||
}
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
private void setNotificationIcons(Notification.Builder notification, Bundle bundle) {
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
|
||||
int smallIconResId = getSmallIconResourceId(smallIcon);
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
|
||||
int largeIconResId = getLargeIconResourceId(largeIcon);
|
||||
final Resources res = mContext.getResources();
|
||||
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
if (largeIconResId != 0 && (largeIconBitmap != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
notification.setLargeIcon(largeIconBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private int getSmallIconResourceId(String iconName) {
|
||||
if (iconName == null) {
|
||||
iconName = "ic_notification";
|
||||
}
|
||||
|
||||
int resourceId = getIconResourceId(iconName);
|
||||
|
||||
if (resourceId == 0) {
|
||||
iconName = "ic_launcher";
|
||||
resourceId = getIconResourceId(iconName);
|
||||
|
||||
if (resourceId == 0) {
|
||||
resourceId = android.R.drawable.ic_dialog_info;
|
||||
}
|
||||
}
|
||||
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
private int getLargeIconResourceId(String iconName) {
|
||||
if (iconName == null) {
|
||||
iconName = "ic_launcher";
|
||||
}
|
||||
|
||||
return getIconResourceId(iconName);
|
||||
}
|
||||
|
||||
private int getIconResourceId(String iconName) {
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
String defType = "mipmap";
|
||||
|
||||
return res.getIdentifier(iconName, defType, packageName);
|
||||
}
|
||||
|
||||
private void setNotificationNumber(Notification.Builder notification, String channelId) {
|
||||
Integer number = channelIdToNotificationCount.get(channelId);
|
||||
if (number == null) {
|
||||
number = 0;
|
||||
}
|
||||
notification.setNumber(number);
|
||||
}
|
||||
|
||||
private void setNotificationMessagingStyle(Notification.Builder notification, Bundle bundle) {
|
||||
Notification.MessagingStyle messagingStyle = getMessagingStyle(bundle);
|
||||
notification.setStyle(messagingStyle);
|
||||
}
|
||||
|
||||
private Notification.MessagingStyle getMessagingStyle(Bundle bundle) {
|
||||
Notification.MessagingStyle messagingStyle;
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("")
|
||||
.build();
|
||||
messagingStyle = new Notification.MessagingStyle(sender);
|
||||
}
|
||||
|
||||
String conversationTitle = getConversationTitle(bundle);
|
||||
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
|
||||
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
|
||||
|
||||
return messagingStyle;
|
||||
}
|
||||
|
||||
private String getConversationTitle(Bundle bundle) {
|
||||
String title = null;
|
||||
|
||||
String version = bundle.getString("version");
|
||||
|
||||
String title = null;
|
||||
if (version != null && version.equals("v2")) {
|
||||
title = bundle.getString("channel_name");
|
||||
} else {
|
||||
@@ -326,103 +192,125 @@ public class CustomPushNotification extends PushNotification {
|
||||
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
private void setMessagingStyleConversationTitle(Notification.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
if (android.text.TextUtils.isEmpty(senderName)) {
|
||||
senderName = getSenderName(bundle);
|
||||
}
|
||||
|
||||
if (conversationTitle != null && (!conversationTitle.startsWith("@") || channelName != senderName)) {
|
||||
messagingStyle.setConversationTitle(conversationTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMessagingStyleMessages(Notification.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
List<Bundle> bundleList;
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
if (bundleArray != null) {
|
||||
bundleList = new ArrayList<Bundle>(bundleArray);
|
||||
String postId = bundle.getString("post_id");
|
||||
String badge = bundle.getString("badge");
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
Bundle b = bundle.getBundle("userInfo");
|
||||
if (b == null) {
|
||||
b = new Bundle();
|
||||
}
|
||||
b.putString("channel_id", channelId);
|
||||
notification.addExtras(b);
|
||||
|
||||
int smallIconResId;
|
||||
int largeIconResId;
|
||||
|
||||
if (smallIcon != null) {
|
||||
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
|
||||
} else {
|
||||
bundleList = new ArrayList<Bundle>();
|
||||
bundleList.add(bundle);
|
||||
smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
}
|
||||
|
||||
int bundleCount = bundleList.size() - 1;
|
||||
for (int i = bundleCount; i >= 0; i--) {
|
||||
Bundle data = bundleList.get(i);
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
|
||||
if (smallIconResId == 0) {
|
||||
smallIconResId = android.R.drawable.ic_dialog_info;
|
||||
}
|
||||
}
|
||||
|
||||
if (largeIcon != null) {
|
||||
largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
|
||||
} else {
|
||||
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
}
|
||||
|
||||
if (badge != null) {
|
||||
int badgeCount = Integer.parseInt(badge);
|
||||
CustomPushNotification.badgeCount = badgeCount;
|
||||
notification.setNumber(badgeCount);
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
} else {
|
||||
// HERE ADD THE DOT INDICATOR STUFF
|
||||
}
|
||||
|
||||
Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("You");
|
||||
if (title != null && !title.startsWith("@")) {
|
||||
messagingStyle
|
||||
.setConversationTitle(title);
|
||||
}
|
||||
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
list.add(bundle);
|
||||
}
|
||||
|
||||
for (Bundle data : list) {
|
||||
String message = data.getString("message");
|
||||
String senderId = data.getString("sender_id");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
if (title == null || !title.startsWith("@")) {
|
||||
message = removeSenderFromMessage(message);
|
||||
}
|
||||
Bundle userInfoBundle = data.getBundle("userInfo");
|
||||
String senderName = getSenderName(data);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationTitle == null || !android.text.TextUtils.isEmpty(senderName.trim())) {
|
||||
message = removeSenderNameFromMessage(message, senderName);
|
||||
}
|
||||
|
||||
long timestamp = data.getLong("time");
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle.addMessage(message, timestamp, senderName);
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName)
|
||||
.build();
|
||||
messagingStyle.addMessage(message, timestamp, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationChannel(Notification.Builder notification, Bundle bundle) {
|
||||
// If Android Oreo or above we need to register a channel
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
messagingStyle.addMessage(message, data.getLong("time"), data.getString("sender_name"));
|
||||
}
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
notification
|
||||
.setContentIntent(intent)
|
||||
.setGroupSummary(true)
|
||||
.setStyle(messagingStyle)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
|
||||
boolean localPushNotificationTest = false;
|
||||
Bundle userInfoBundle = bundle.getBundle("userInfo");
|
||||
if (userInfoBundle != null) {
|
||||
localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
}
|
||||
|
||||
if (mAppLifecycleFacade.isAppVisible() && !localPushNotificationTest) {
|
||||
notificationChannel = mMinImportanceChannel;
|
||||
}
|
||||
|
||||
notification.setChannelId(notificationChannel.getId());
|
||||
}
|
||||
|
||||
private void setNotificationBadgeIconType(Notification.Builder notification) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setBadgeIconType(Notification.BADGE_ICON_SMALL);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationGroup(Notification.Builder notification) {
|
||||
// Let's add a delete intent when the notification is dismissed
|
||||
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
|
||||
delIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
notification
|
||||
.setGroup(GROUP_KEY_MESSAGES)
|
||||
.setGroupSummary(true);
|
||||
notification.setGroup(GROUP_KEY_MESSAGES);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
|
||||
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||
.setLabel("Reply")
|
||||
.build();
|
||||
|
||||
Notification.Action replyAction = new Notification.Action.Builder(
|
||||
R.drawable.ic_notif_action_reply, "Reply", replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.build();
|
||||
|
||||
notification
|
||||
.setShowWhen(true)
|
||||
.addAction(replyAction);
|
||||
}
|
||||
|
||||
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
|
||||
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
|
||||
notification.setLargeIcon(largeIconBitmap);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationSound(Notification.Builder notification, NotificationPreferences notificationPreferences) {
|
||||
String soundUri = notificationPreferences.getNotificationSound();
|
||||
if (soundUri != null) {
|
||||
if (soundUri != "none") {
|
||||
@@ -432,120 +320,59 @@ public class CustomPushNotification extends PushNotification {
|
||||
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
|
||||
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationVibrate(Notification.Builder notification, NotificationPreferences notificationPreferences) {
|
||||
boolean vibrate = notificationPreferences.getShouldVibrate();
|
||||
if (vibrate) {
|
||||
// Use the system default for vibration
|
||||
// use the system default for vibration
|
||||
notification.setDefaults(Notification.DEFAULT_VIBRATE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationBlink(Notification.Builder notification, NotificationPreferences notificationPreferences) {
|
||||
boolean blink = notificationPreferences.getShouldBlink();
|
||||
if (blink) {
|
||||
notification.setLights(Color.CYAN, 500, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationDeleteIntent(Notification.Builder notification, int notificationId) {
|
||||
// Let's add a delete intent when the notification is dismissed
|
||||
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
|
||||
delIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
}
|
||||
|
||||
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||
String postId = bundle.getString("post_id");
|
||||
if (postId == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
|
||||
mContext,
|
||||
notificationId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||
.setLabel("Reply")
|
||||
.build();
|
||||
|
||||
int icon = R.drawable.ic_notif_action_reply;
|
||||
CharSequence title = "Reply";
|
||||
Notification.Action replyAction = new Notification.Action.Builder(icon, title, replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.build();
|
||||
|
||||
notification
|
||||
.setShowWhen(true)
|
||||
.addAction(replyAction);
|
||||
return notification;
|
||||
}
|
||||
|
||||
private void notifyReceivedToJS() {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private void cancelNotification(Bundle data, int notificationId) {
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String badge = data.getString("badge");
|
||||
|
||||
CustomPushNotification.badgeCount = Integer.parseInt(badge);
|
||||
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
|
||||
}
|
||||
|
||||
private String getSenderName(Bundle bundle) {
|
||||
String senderName = bundle.getString("sender_name");
|
||||
if (senderName != null) {
|
||||
return senderName;
|
||||
public static Integer getMessageCountInChannel(String channelId) {
|
||||
Object objCount = channelIdToNotificationCount.get(channelId);
|
||||
if (objCount != null) {
|
||||
return (Integer)objCount;
|
||||
}
|
||||
|
||||
String channelName = bundle.getString("channel_name");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void cancelNotification(Bundle data, int notificationId) {
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String numberString = data.getString("badge");
|
||||
|
||||
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
|
||||
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
|
||||
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
}
|
||||
|
||||
private String getSenderName(String channelName, String message) {
|
||||
if (channelName != null && channelName.startsWith("@")) {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
String message = bundle.getString("message");
|
||||
if (message != null) {
|
||||
String name = message.split(":")[0];
|
||||
if (name != message) {
|
||||
return name;
|
||||
}
|
||||
String senderName = message.split(":")[0];
|
||||
if (senderName != message) {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
return getConversationTitle(bundle);
|
||||
return " ";
|
||||
}
|
||||
|
||||
private String removeSenderNameFromMessage(String message, String senderName) {
|
||||
return message.replaceFirst(senderName, "").replaceFirst(": ", "").trim();
|
||||
}
|
||||
|
||||
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
ReceiptDelivery.send(context, ackId, postId, type, isIdLoaded, promise);
|
||||
}
|
||||
|
||||
private void createNotificationChannels() {
|
||||
// Notification channels are not supported in Android Nougat and below
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
mHighImportanceChannel = new NotificationChannel("channel_01", "High Importance", NotificationManager.IMPORTANCE_HIGH);
|
||||
mHighImportanceChannel.setShowBadge(true);
|
||||
notificationManager.createNotificationChannel(mHighImportanceChannel);
|
||||
|
||||
mMinImportanceChannel = new NotificationChannel("channel_02", "Min Importance", NotificationManager.IMPORTANCE_MIN);
|
||||
mMinImportanceChannel.setShowBadge(true);
|
||||
notificationManager.createNotificationChannel(mMinImportanceChannel);
|
||||
private String removeSenderFromMessage(String message) {
|
||||
String sender = String.format("%s: ", getSenderName("", message));
|
||||
return message.replaceFirst(sender, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.PushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
||||
|
||||
import com.wix.reactnativenotifications.helpers.PushNotificationHelper;
|
||||
import static com.wix.reactnativenotifications.Defs.LOGTAG;
|
||||
|
||||
public class CustomPushNotificationDrawer extends PushNotificationsDrawer {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.mattermost.react_native_interface.AsyncStorageHelper;
|
||||
import com.mattermost.react_native_interface.KeysReadableArray;
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class InitializationModule extends ReactContextBaseJavaModule {
|
||||
|
||||
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
|
||||
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
|
||||
static final String APP_BACKGROUND = "APP_BACKGROUND";
|
||||
|
||||
private final Application mApplication;
|
||||
|
||||
public InitializationModule(Application application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Initialization";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
Map<String, Object> constants = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Package all native module variables in constants
|
||||
* in order to avoid the native bridge
|
||||
*
|
||||
* KeyStore:
|
||||
* credentialsExist
|
||||
* deviceToken
|
||||
* currentUserId
|
||||
* token
|
||||
* url
|
||||
*
|
||||
* AsyncStorage:
|
||||
* toolbarBackground
|
||||
* toolbarTextColor
|
||||
* appBackground
|
||||
*
|
||||
* Miscellaneous:
|
||||
* MattermostManaged.Config
|
||||
* replyFromPushNotification
|
||||
*/
|
||||
|
||||
MainApplication app = (MainApplication) mApplication;
|
||||
final Boolean[] credentialsExist = {false};
|
||||
final WritableMap[] credentials = {null};
|
||||
final Object[] config = {null};
|
||||
|
||||
// Get KeyStore credentials
|
||||
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
|
||||
module.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
credentialsExist[0] = false;
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
credentialsExist[0] = true;
|
||||
credentials[0] = map;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get managedConfig from MattermostManagedModule
|
||||
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
WritableNativeMap nativeMap = (WritableNativeMap) value;
|
||||
config[0] = value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get AsyncStorage key/values
|
||||
final ArrayList<String> keys = new ArrayList<String>(5);
|
||||
keys.add(TOOLBAR_BACKGROUND);
|
||||
keys.add(TOOLBAR_TEXT_COLOR);
|
||||
keys.add(APP_BACKGROUND);
|
||||
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return keys.get(index);
|
||||
}
|
||||
};
|
||||
|
||||
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
|
||||
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
|
||||
|
||||
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
|
||||
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
|
||||
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
|
||||
|
||||
if (toolbarBackground != null
|
||||
&& toolbarTextColor != null
|
||||
&& appBackground != null) {
|
||||
|
||||
constants.put("themesExist", true);
|
||||
constants.put("toolbarBackground", toolbarBackground);
|
||||
constants.put("toolbarTextColor", toolbarTextColor);
|
||||
constants.put("appBackground", appBackground);
|
||||
} else {
|
||||
constants.put("themesExist", false);
|
||||
}
|
||||
|
||||
|
||||
if (credentialsExist[0]) {
|
||||
constants.put("credentialsExist", true);
|
||||
constants.put("credentials", credentials[0]);
|
||||
} else {
|
||||
constants.put("credentialsExist", false);
|
||||
}
|
||||
|
||||
constants.put("managedConfig", config[0]);
|
||||
constants.put("replyFromPushNotification", app.replyFromPushNotification);
|
||||
app.replyFromPushNotification = false;
|
||||
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class InitializationPackage implements ReactPackage {
|
||||
|
||||
private final Application mApplication;
|
||||
|
||||
public InitializationPackage(Application application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.reactnativenavigation.controllers.SplashActivity;
|
||||
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
public class MainActivity extends SplashActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.launch_screen);
|
||||
|
||||
/**
|
||||
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
|
||||
* 1. Open app from launcher/appDrawer
|
||||
* 2. Go home
|
||||
* 3. Send notification and open
|
||||
* 4. It creates a new Activity and Destroys the old
|
||||
* 5. Causing an unnecessary app restart
|
||||
* 6. This solution short-circuits the restart
|
||||
*/
|
||||
if (!isTaskRoot()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSplashLayout() {
|
||||
return R.layout.launch_screen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,42 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.mattermost.share.SharePackage;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.learnium.RNDeviceInfo.RNDeviceModule;
|
||||
import com.imagepicker.ImagePickerModule;
|
||||
import com.psykar.cookiemanager.CookieManagerModule;
|
||||
import com.oblador.vectoricons.VectorIconsModule;
|
||||
import com.wix.reactnativenotifications.RNNotificationsModule;
|
||||
import io.tradle.react.LocalAuthModule;
|
||||
import com.gantix.JailMonkey.JailMonkeyModule;
|
||||
import com.RNFetchBlob.RNFetchBlob;
|
||||
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule;
|
||||
import com.inprogress.reactnativeyoutube.YouTubeStandaloneModule;
|
||||
import com.philipphecht.RNDocViewerModule;
|
||||
import io.github.elyx0.reactnativedocumentpicker.DocumentPickerModule;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
|
||||
import com.reactnativecommunity.netinfo.NetInfoModule;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import io.sentry.RNSentryModule;
|
||||
import com.dylanvann.fastimage.FastImageViewPackage;
|
||||
import com.levelasquez.androidopensettings.AndroidOpenSettings;
|
||||
import com.mkuczera.RNReactNativeHapticFeedbackModule;
|
||||
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
|
||||
import com.oblador.keychain.KeychainPackage;
|
||||
import com.reactlibrary.RNReactNativeDocViewerPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
import com.horcrux.svg.SvgPackage;
|
||||
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
|
||||
import io.sentry.RNSentryPackage;
|
||||
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
import com.gantix.JailMonkey.JailMonkeyPackage;
|
||||
import io.tradle.react.LocalAuthPackage;
|
||||
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.imagepicker.ImagePickerPackage;
|
||||
import com.gnet.bottomsheet.RNBottomSheetPackage;
|
||||
import com.learnium.RNDeviceInfo.RNDeviceInfo;
|
||||
import com.psykar.cookiemanager.CookieManagerPackage;
|
||||
import com.oblador.vectoricons.VectorIconsPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.reactnativenavigation.react.NavigationReactNativeHost;
|
||||
import com.reactnativenavigation.react.ReactGateway;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
@@ -51,48 +45,12 @@ import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.TurboReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMarker;
|
||||
import com.facebook.react.bridge.ReactMarkerConstants;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.module.model.ReactModuleInfo;
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
import android.util.Log;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
|
||||
public static MainApplication instance;
|
||||
|
||||
public NotificationsLifecycleFacade notificationsLifecycleFacade;
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
|
||||
public long APP_START_TIME;
|
||||
|
||||
public long RELOAD;
|
||||
public long CONTENT_APPEARED;
|
||||
|
||||
public long PROCESS_PACKAGES_START;
|
||||
public long PROCESS_PACKAGES_END;
|
||||
|
||||
private Bundle mManagedConfig = null;
|
||||
|
||||
@Override
|
||||
protected ReactGateway createReactGateway() {
|
||||
ReactNativeHost host = new NavigationReactNativeHost(this, isDebug(), createAdditionalReactPackages()) {
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
};
|
||||
return new ReactGateway(this, isDebug(), host);
|
||||
}
|
||||
public Boolean replyFromPushNotification = false;
|
||||
|
||||
@Override
|
||||
public boolean isDebug() {
|
||||
@@ -105,103 +63,38 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
// Add the packages you require here.
|
||||
// No need to add RnnPackage and MainReactPackage
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new TurboReactPackage() {
|
||||
@Override
|
||||
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
||||
switch (name) {
|
||||
case "MattermostShare":
|
||||
return new ShareModule(instance, reactContext);
|
||||
case "RNDeviceInfo":
|
||||
return new RNDeviceModule(reactContext, false);
|
||||
case "ImagePickerManager":
|
||||
return new ImagePickerModule(reactContext, R.style.DefaultExplainingPermissionsTheme);
|
||||
case "RNCookieManagerAndroid":
|
||||
return new CookieManagerModule(reactContext);
|
||||
case "RNVectorIconsModule":
|
||||
return new VectorIconsModule(reactContext);
|
||||
case "WixRNNotifications":
|
||||
return new RNNotificationsModule(instance, reactContext);
|
||||
case "RNLocalAuth":
|
||||
return new LocalAuthModule(reactContext);
|
||||
case "JailMonkey":
|
||||
return new JailMonkeyModule(reactContext, false);
|
||||
case "RNFetchBlob":
|
||||
return new RNFetchBlob(reactContext);
|
||||
case "MattermostManaged":
|
||||
return MattermostManagedModule.getInstance(reactContext);
|
||||
case "NotificationPreferences":
|
||||
return NotificationPreferencesModule.getInstance(instance, reactContext);
|
||||
case "RNTextInputReset":
|
||||
return new RNTextInputResetModule(reactContext);
|
||||
case "ReactNativeExceptionHandler":
|
||||
return new ReactNativeExceptionHandlerModule(reactContext);
|
||||
case "YouTubeStandaloneModule":
|
||||
return new YouTubeStandaloneModule(reactContext);
|
||||
case "RNDocViewer":
|
||||
return new RNDocViewerModule(reactContext);
|
||||
case "RNDocumentPicker":
|
||||
return new DocumentPickerModule(reactContext);
|
||||
case "RNKeychainManager":
|
||||
return new KeychainModule(reactContext);
|
||||
case "RNSentry":
|
||||
return new RNSentryModule(reactContext);
|
||||
case AsyncStorageModule.NAME:
|
||||
return new AsyncStorageModule(reactContext);
|
||||
case NetInfoModule.NAME:
|
||||
return new NetInfoModule(reactContext);
|
||||
case "RNAndroidOpenSettings":
|
||||
return new AndroidOpenSettings(reactContext);
|
||||
case "RNReactNativeHapticFeedbackModule":
|
||||
return new RNReactNativeHapticFeedbackModule(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
||||
return new ReactModuleInfoProvider() {
|
||||
@Override
|
||||
public Map<String, ReactModuleInfo> getReactModuleInfos() {
|
||||
Map<String, ReactModuleInfo> map = new HashMap<>();
|
||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
|
||||
map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false));
|
||||
|
||||
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
|
||||
map.put("RNDeviceInfo", new ReactModuleInfo("RNDeviceInfo", "com.learnium.RNDeviceInfo.RNDeviceModule", false, false, true, false, false));
|
||||
map.put("ImagePickerManager", new ReactModuleInfo("ImagePickerManager", "com.imagepicker.ImagePickerModule", false, false, false, false, false));
|
||||
map.put("RNCookieManagerAndroid", new ReactModuleInfo("RNCookieManagerAndroid", "com.psykar.cookiemanager.CookieManagerModule", false, false, false, false, false));
|
||||
map.put("RNVectorIconsModule", new ReactModuleInfo("RNVectorIconsModule", "com.oblador.vectoricons.VectorIconsModule", false, false, false, false, false));
|
||||
map.put("WixRNNotifications", new ReactModuleInfo("WixRNNotifications", "com.wix.reactnativenotifications.RNNotificationsModule", false, false, false, false, false));
|
||||
map.put("RNLocalAuth", new ReactModuleInfo("RNLocalAuth", "io.tradle.react.LocalAuthModule", false, false, false, false, false));
|
||||
map.put("JailMonkey", new ReactModuleInfo("JailMonkey", "com.gantix.JailMonkey.JailMonkeyModule", false, false, true, false, false));
|
||||
map.put("RNFetchBlob", new ReactModuleInfo("RNFetchBlob", "com.RNFetchBlob.RNFetchBlob", false, false, true, false, false));
|
||||
map.put("ReactNativeExceptionHandler", new ReactModuleInfo("ReactNativeExceptionHandler", "com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule", false, false, false, false, false));
|
||||
map.put("YouTubeStandaloneModule", new ReactModuleInfo("YouTubeStandaloneModule", "com.inprogress.reactnativeyoutube.YouTubeStandaloneModule", false, false, false, false, false));
|
||||
map.put("RNDocViewer", new ReactModuleInfo("RNDocViewer", "com.philipphecht.RNDocViewerModule", false, false, false, false, false));
|
||||
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "io.github.elyx0.reactnativedocumentpicker.DocumentPickerModule", false, false, false, false, false));
|
||||
map.put("RNKeychainManager", new ReactModuleInfo("RNKeychainManager", "com.oblador.keychain.KeychainModule", false, false, true, false, false));
|
||||
map.put("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
|
||||
map.put(AsyncStorageModule.NAME, new ReactModuleInfo(AsyncStorageModule.NAME, "com.reactnativecommunity.asyncstorage.AsyncStorageModule", false, false, false, false, false));
|
||||
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
|
||||
map.put("RNAndroidOpenSettings", new ReactModuleInfo("RNAndroidOpenSettings", "com.levelasquez.androidopensettings.AndroidOpenSettings", false, false, false, false, false));
|
||||
map.put("RNReactNativeHapticFeedbackModule", new ReactModuleInfo("RNReactNativeHapticFeedback", "com.mkuczera.RNReactNativeHapticFeedbackModule", false, false, false, false, false));
|
||||
return map;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
new FastImageViewPackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new ImagePickerPackage(),
|
||||
new RNBottomSheetPackage(),
|
||||
new RNDeviceInfo(),
|
||||
new CookieManagerPackage(),
|
||||
new VectorIconsPackage(),
|
||||
new SvgPackage(),
|
||||
new LinearGradientPackage(),
|
||||
new RNNotificationsPackage(this),
|
||||
new LocalAuthPackage(),
|
||||
new JailMonkeyPackage(),
|
||||
new RNFetchBlobPackage(),
|
||||
new MattermostPackage(this),
|
||||
new RNSentryPackage(),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube(),
|
||||
new ReactVideoPackage(),
|
||||
new RNGestureHandlerPackage(),
|
||||
new RNPasteableTextInputPackage()
|
||||
new RNReactNativeDocViewerPackage(),
|
||||
new ReactNativeDocumentPicker(),
|
||||
new SharePackage(this),
|
||||
new KeychainPackage(),
|
||||
new InitializationPackage(this),
|
||||
new RNRecyclerviewListPackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new RNGestureHandlerPackage()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@@ -212,10 +105,19 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
RealPathUtil.deleteTempFiles(tempFolder);
|
||||
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
// Create an object of the custom facade impl
|
||||
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
|
||||
// Attach it to react-native-navigation
|
||||
setActivityCallbacks(notificationsLifecycleFacade);
|
||||
|
||||
// Uncomment to listen to react markers for build that has telemetry enabled
|
||||
// addReactMarkerListener();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearHostOnActivityDestroy(Activity activity) {
|
||||
// This solves the issue where the splash screen does not go away
|
||||
// after the app is killed by the OS cause of memory or a long time in the background
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -223,7 +125,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return new CustomPushNotification(
|
||||
context,
|
||||
bundle,
|
||||
defaultFacade,
|
||||
notificationsLifecycleFacade, // Instead of defaultFacade!!!
|
||||
defaultAppLaunchHelper,
|
||||
new JsIOHelper()
|
||||
);
|
||||
@@ -233,81 +135,4 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
|
||||
}
|
||||
|
||||
public ReactContext getRunningReactContext() {
|
||||
final ReactGateway reactGateway = getReactGateway();
|
||||
|
||||
if (reactGateway == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return reactGateway
|
||||
.getReactNativeHost()
|
||||
.getReactInstanceManager()
|
||||
.getCurrentReactContext();
|
||||
}
|
||||
|
||||
public synchronized Bundle loadManagedConfig(Context ctx) {
|
||||
if (ctx != null) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
mManagedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
myRestrictionsMgr = null;
|
||||
|
||||
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
|
||||
return mManagedConfig;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized Bundle getManagedConfig() {
|
||||
if (mManagedConfig != null && mManagedConfig.size() > 0) {
|
||||
return mManagedConfig;
|
||||
}
|
||||
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
return loadManagedConfig(ctx);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addReactMarkerListener() {
|
||||
ReactMarker.addListener(new ReactMarker.MarkerListener() {
|
||||
@Override
|
||||
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
|
||||
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
|
||||
APP_START_TIME = System.currentTimeMillis();
|
||||
RELOAD = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
|
||||
PROCESS_PACKAGES_START = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
|
||||
PROCESS_PACKAGES_END = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
|
||||
CONTENT_APPEARED = System.currentTimeMillis();
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
map.putDouble("appReload", RELOAD);
|
||||
map.putDouble("appContentAppeared", CONTENT_APPEARED);
|
||||
|
||||
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
|
||||
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
|
||||
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("nativeMetrics", map);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.mattermost.react_native_interface.AsyncStorageHelper;
|
||||
import com.mattermost.react_native_interface.KeysReadableArray;
|
||||
|
||||
public class MattermostCredentialsHelper {
|
||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
||||
|
||||
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
|
||||
final KeychainModule keychainModule = new KeychainModule(context);
|
||||
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
|
||||
final ArrayList<String> keys = new ArrayList<String>(1);
|
||||
keys.add(CURRENT_SERVER_URL);
|
||||
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return keys.get(index);
|
||||
}
|
||||
};
|
||||
|
||||
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
|
||||
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
|
||||
|
||||
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,8 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
@@ -21,35 +11,14 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.LifecycleEventListener;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static MattermostManagedModule instance;
|
||||
|
||||
private static final String TAG = MattermostManagedModule.class.getSimpleName();
|
||||
|
||||
private final IntentFilter restrictionsFilter =
|
||||
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
|
||||
|
||||
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
|
||||
@Override public void onReceive(Context ctx, Intent intent) {
|
||||
if (ctx != null) {
|
||||
Bundle managedConfig = MainApplication.instance.loadManagedConfig(ctx);
|
||||
|
||||
// Check current configuration settings, change your app's UI and
|
||||
// functionality as necessary.
|
||||
Log.i(TAG, "Managed Configuration Changed");
|
||||
sendConfigChanged(managedConfig);
|
||||
handleBlurScreen(managedConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
private boolean shouldBlurAppScreen = false;
|
||||
|
||||
private MattermostManagedModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
reactContext.addLifecycleEventListener(this);
|
||||
}
|
||||
|
||||
public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) {
|
||||
@@ -69,10 +38,19 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
|
||||
return "MattermostManaged";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void blurAppScreen(boolean enabled) {
|
||||
shouldBlurAppScreen = enabled;
|
||||
}
|
||||
|
||||
public boolean isBlurAppScreenEnabled() {
|
||||
return shouldBlurAppScreen;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getConfig(final Promise promise) {
|
||||
try {
|
||||
Bundle config = MainApplication.instance.getManagedConfig();
|
||||
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
|
||||
|
||||
if (config != null) {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
@@ -84,140 +62,4 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
|
||||
promise.resolve(Arguments.createMap());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
// Close the current activity and open the security settings.
|
||||
public void goToSecuritySettings() {
|
||||
Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getReactApplicationContext().startActivity(intent);
|
||||
|
||||
getCurrentActivity().finish();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isRunningInSplitView(final Promise promise) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
Activity current = getCurrentActivity();
|
||||
if (current != null) {
|
||||
result.putBoolean("isSplitView", current.isInMultiWindowMode());
|
||||
} else {
|
||||
result.putBoolean("isSplitView", false);
|
||||
}
|
||||
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void quitApp() {
|
||||
getCurrentActivity().finish();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostResume() {
|
||||
Activity activity = getCurrentActivity();
|
||||
Bundle managedConfig = MainApplication.instance.getManagedConfig();
|
||||
|
||||
if (activity != null && managedConfig != null) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
|
||||
ReactContext ctx = MainApplication.instance.getRunningReactContext();
|
||||
Bundle newManagedConfig = null;
|
||||
if (ctx != null) {
|
||||
newManagedConfig = MainApplication.instance.loadManagedConfig(ctx);
|
||||
if (!equalBundles(newManagedConfig, managedConfig)) {
|
||||
Log.i(TAG, "onResumed Managed Configuration Changed");
|
||||
sendConfigChanged(newManagedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
handleBlurScreen(newManagedConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
Activity activity = getCurrentActivity();
|
||||
Bundle managedConfig = MainApplication.instance.getManagedConfig();
|
||||
|
||||
if (activity != null && managedConfig != null) {
|
||||
try {
|
||||
activity.unregisterReceiver(restrictionsReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Just ignore this cause the receiver wasn't registered for this activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
}
|
||||
|
||||
private void handleBlurScreen(Bundle config) {
|
||||
Activity activity = getCurrentActivity();
|
||||
boolean blurAppScreen = false;
|
||||
|
||||
if (config != null) {
|
||||
blurAppScreen = Boolean.parseBoolean(config.getString("blurApplicationScreen"));
|
||||
}
|
||||
|
||||
if (blurAppScreen) {
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
|
||||
} else {
|
||||
activity.getWindow().clearFlags(LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendConfigChanged(Bundle config) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
if (config != null) {
|
||||
result = Arguments.fromBundle(config);
|
||||
}
|
||||
ReactContext ctx = MainApplication.instance.getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit("managedConfigDidChange", result);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean equalBundles(Bundle one, Bundle two) {
|
||||
if (one == null && two == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (one == null || two == null)
|
||||
return false;
|
||||
|
||||
if(one.size() != two.size())
|
||||
return false;
|
||||
|
||||
Set<String> setOne = new ArraySet<String>();
|
||||
setOne.addAll(one.keySet());
|
||||
setOne.addAll(two.keySet());
|
||||
Object valueOne;
|
||||
Object valueTwo;
|
||||
|
||||
for(String key : setOne) {
|
||||
if (!one.containsKey(key) || !two.containsKey(key))
|
||||
return false;
|
||||
|
||||
valueOne = one.get(key);
|
||||
valueTwo = two.get(key);
|
||||
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
|
||||
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
|
||||
return false;
|
||||
}
|
||||
else if(valueOne == null) {
|
||||
if(valueTwo != null)
|
||||
return false;
|
||||
}
|
||||
else if(!valueOne.equals(valueTwo))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
|
||||
public class MattermostPackage implements ReactPackage {
|
||||
private final MainApplication mApplication;
|
||||
|
||||
public MattermostPackage(MainApplication application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(
|
||||
MattermostManagedModule.getInstance(reactContext),
|
||||
NotificationPreferencesModule.getInstance(mApplication, reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -21,12 +21,10 @@ import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
@@ -38,32 +36,36 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mContext = context;
|
||||
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
|
||||
|
||||
|
||||
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
||||
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
String token = map.getString("password");
|
||||
String serverUrl = map.getString("service");
|
||||
String[] credentials = map.getString("password").split(",[ ]*");
|
||||
String token = null;
|
||||
String serverUrl = null;
|
||||
if (credentials.length == 2) {
|
||||
token = credentials[0];
|
||||
serverUrl = credentials[1];
|
||||
|
||||
Log.i("ReactNative", String.format("URL=%s", serverUrl));
|
||||
}
|
||||
|
||||
Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token));
|
||||
replyToMessage(serverUrl, token, notificationId, message);
|
||||
}
|
||||
}
|
||||
@@ -73,11 +75,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String postId = bundle.getString("post_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
if (android.text.TextUtils.isEmpty(rootId)) {
|
||||
rootId = postId;
|
||||
}
|
||||
final String rootId = bundle.getString("post_id");
|
||||
|
||||
if (token == null || serverUrl == null) {
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
@@ -87,6 +85,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
String json = buildReplyPost(channelId, rootId, message.toString());
|
||||
Log.i("ReactNative", String.format("JSON STRING %s", json));
|
||||
RequestBody body = RequestBody.create(JSON, json);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
@@ -98,7 +97,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
client.newCall(request).enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
|
||||
Log.i("ReactNative", String.format("Reply with message %s FAILED exception %s", message, e.getMessage()));
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
}
|
||||
|
||||
@@ -106,9 +105,9 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
onReplySuccess(notificationManager, notificationId, channelId);
|
||||
Log.i("ReactNative", "Reply SUCCESS");
|
||||
Log.i("ReactNative", String.format("Reply with message %s", message));
|
||||
} else {
|
||||
Log.i("ReactNative", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
|
||||
Log.i("ReactNative", String.format("Reply with message %s FAILED status %s BODY %s", message, response.code(), response.body().string()));
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
}
|
||||
}
|
||||
@@ -116,15 +115,11 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
}
|
||||
|
||||
protected String buildReplyPost(String channelId, String rootId, String message) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("channel_id", channelId);
|
||||
json.put("message", message);
|
||||
json.put("root_id", rootId);
|
||||
return json.toString();
|
||||
} catch(JSONException e) {
|
||||
return "{}";
|
||||
}
|
||||
return "{"
|
||||
+ "\"channel_id\": \"" + channelId + "\","
|
||||
+ "\"message\": \"" + message + "\","
|
||||
+ "\"root_id\": \"" + rootId + "\""
|
||||
+ "}";
|
||||
}
|
||||
|
||||
protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) {
|
||||
@@ -133,16 +128,12 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
Bundle userInfoBundle = new Bundle();
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Message failed to send.")
|
||||
.setSmallIcon(smallIconResId)
|
||||
.addExtras(userInfoBundle)
|
||||
.build();
|
||||
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.ArraySet;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.reactnativenavigation.controllers.ActivityCallbacks;
|
||||
import com.reactnativenavigation.react.ReactGateway;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
|
||||
public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade {
|
||||
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
|
||||
private static NotificationsLifecycleFacade instance;
|
||||
|
||||
private Bundle managedConfig = null;
|
||||
private Activity mVisibleActivity;
|
||||
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
|
||||
|
||||
private final IntentFilter restrictionsFilter =
|
||||
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
|
||||
|
||||
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
|
||||
@Override public void onReceive(Context context, Intent intent) {
|
||||
if (context != null) {
|
||||
// Get the current configuration bundle
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) context
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
|
||||
// Check current configuration settings, change your app's UI and
|
||||
// functionality as necessary.
|
||||
Log.i("ReactNative", "Managed Configuration Changed");
|
||||
sendConfigChanged(managedConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static NotificationsLifecycleFacade getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new NotificationsLifecycleFacade();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
|
||||
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
|
||||
LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
|
||||
if (activity != null) {
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
switchToVisible(activity);
|
||||
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
if (managedConfig != null && managedConfig.size() > 0 && ctx != null) {
|
||||
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) ctx
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
if (!equalBundles(newConfig ,managedConfig)) {
|
||||
Log.i("ReactNative", "onResumed Managed Configuration Changed");
|
||||
managedConfig = newConfig;
|
||||
sendConfigChanged(managedConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
switchToInvisible(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
switchToInvisible(activity);
|
||||
if (managedConfig != null && managedConfig.size() > 0) {
|
||||
try {
|
||||
activity.unregisterReceiver(restrictionsReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Just ignore this cause the receiver wasn't registered for this activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
switchToInvisible(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReactInitialized() {
|
||||
return NavigationApplication.instance.isReactContextInitialized();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactContext getRunningReactContext() {
|
||||
final ReactGateway reactGateway = NavigationApplication.instance.getReactGateway();
|
||||
if (reactGateway == null || !reactGateway.isInitialized()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return reactGateway.getReactContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAppVisible() {
|
||||
return mVisibleActivity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void addVisibilityListener(AppVisibilityListener listener) {
|
||||
mListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void removeVisibilityListener(AppVisibilityListener listener) {
|
||||
mListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
if (mVisibleActivity != null) {
|
||||
Intent intent = new Intent("onConfigurationChanged");
|
||||
intent.putExtra("newConfig", newConfig);
|
||||
mVisibleActivity.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void switchToVisible(Activity activity) {
|
||||
if (mVisibleActivity == null) {
|
||||
mVisibleActivity = activity;
|
||||
Log.v(TAG, "Activity is now visible ("+activity+")");
|
||||
for (AppVisibilityListener listener : mListeners) {
|
||||
listener.onAppVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void switchToInvisible(Activity activity) {
|
||||
if (mVisibleActivity == activity) {
|
||||
mVisibleActivity = null;
|
||||
Log.v(TAG, "Activity is now NOT visible ("+activity+")");
|
||||
for (AppVisibilityListener listener : mListeners) {
|
||||
listener.onAppNotVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void LoadManagedConfig(ReactContext ctx) {
|
||||
if (ctx != null) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) ctx
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
myRestrictionsMgr = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized Bundle getManagedConfig() {
|
||||
if (managedConfig!= null && managedConfig.size() > 0) {
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
LoadManagedConfig(ctx);
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void sendConfigChanged(Bundle config) {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
if (ctx != null) {
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("managedConfigDidChange", result);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean equalBundles(Bundle one, Bundle two) {
|
||||
if (one == null || two == null)
|
||||
return false;
|
||||
|
||||
if(one.size() != two.size())
|
||||
return false;
|
||||
|
||||
Set<String> setOne = new ArraySet<String>();
|
||||
setOne.addAll(one.keySet());
|
||||
setOne.addAll(two.keySet());
|
||||
Object valueOne;
|
||||
Object valueTwo;
|
||||
|
||||
for(String key : setOne) {
|
||||
if (!one.containsKey(key) || !two.containsKey(key))
|
||||
return false;
|
||||
|
||||
valueOne = one.get(key);
|
||||
valueTwo = two.get(key);
|
||||
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
|
||||
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
|
||||
return false;
|
||||
}
|
||||
else if(valueOne == null) {
|
||||
if(valueTwo != null)
|
||||
return false;
|
||||
}
|
||||
else if(!valueOne.equals(valueTwo))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public interface RNEditTextOnPasteListener {
|
||||
void onPaste(Uri itemUri);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class RNPasteableActionCallback implements ActionMode.Callback {
|
||||
|
||||
private RNPasteableEditText mEditText;
|
||||
|
||||
RNPasteableActionCallback(RNPasteableEditText editText) {
|
||||
mEditText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
Bundle config = MainApplication.instance.getManagedConfig();
|
||||
if (config != null) {
|
||||
WritableMap result = Arguments.fromBundle(config);
|
||||
String copyPasteProtection = result.getString("copyAndPasteProtection");
|
||||
if (copyPasteProtection.equals("true")) {
|
||||
disableMenus(menu);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Uri uri = this.getUriInClipboard();
|
||||
if (item.getItemId() == android.R.id.paste && uri != null) {
|
||||
mEditText.getOnPasteListener().onPaste(uri);
|
||||
mode.finish();
|
||||
} else {
|
||||
mEditText.onTextContextMenuItem(item.getItemId());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
|
||||
}
|
||||
|
||||
private void disableMenus(Menu menu) {
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
int id = item.getItemId();
|
||||
boolean shouldDisableMenu = (
|
||||
id == android.R.id.paste
|
||||
|| id == android.R.id.copy
|
||||
|| id == android.R.id.cut
|
||||
);
|
||||
item.setEnabled(!shouldDisableMenu);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getUriInClipboard() {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) mEditText.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClipData.Item item = clipData.getItemAt(0);
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = item.getText().toString();
|
||||
if (text.length() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.getUri();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
public class RNPasteableEditText extends ReactEditText {
|
||||
|
||||
private RNEditTextOnPasteListener mOnPasteListener;
|
||||
|
||||
public RNPasteableEditText(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setOnPasteListener(RNEditTextOnPasteListener listener) {
|
||||
mOnPasteListener = listener;
|
||||
}
|
||||
|
||||
public RNEditTextOnPasteListener getOnPasteListener() {
|
||||
return mOnPasteListener;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.net.Uri;
|
||||
import android.util.Patterns;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
|
||||
|
||||
private RNPasteableEditText mEditText;
|
||||
|
||||
RNPasteableEditTextOnPasteListener(RNPasteableEditText editText) {
|
||||
mEditText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaste(Uri itemUri) {
|
||||
ReactContext reactContext = (ReactContext)mEditText.getContext();
|
||||
String uri = itemUri.toString();
|
||||
|
||||
WritableArray images = null;
|
||||
WritableMap error = null;
|
||||
|
||||
String uriMimeType = reactContext.getContentResolver().getType(itemUri);
|
||||
if (uriMimeType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handle for Google docs
|
||||
if (uri.equals("content://com.google.android.apps.docs.editors.kix.editors.clipboard")) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) reactContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClipData.Item item = clipData.getItemAt(0);
|
||||
String htmlText = item.getHtmlText();
|
||||
// Find uri from html
|
||||
Matcher matcher = Patterns.WEB_URL.matcher(htmlText);
|
||||
if (matcher.find()) {
|
||||
uri = htmlText.substring(matcher.start(1), matcher.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (uri.startsWith("http")) {
|
||||
Thread pastImageFromUrlThread = new Thread(new RNPasteableImageFromUrl(reactContext, mEditText, uri));
|
||||
pastImageFromUrlThread.start();
|
||||
return;
|
||||
}
|
||||
|
||||
uri = RealPathUtil.getRealPathFromURI(reactContext, itemUri);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get type
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (extension == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileName
|
||||
String fileName = URLUtil.guessFileName(uri, null, mimeType);
|
||||
|
||||
// Get fileSize
|
||||
long fileSize;
|
||||
try {
|
||||
ContentResolver contentResolver = reactContext.getContentResolver();
|
||||
AssetFileDescriptor assetFileDescriptor = contentResolver.openAssetFileDescriptor(itemUri, "r");
|
||||
if (assetFileDescriptor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileSize = assetFileDescriptor.getLength();
|
||||
|
||||
WritableMap image = Arguments.createMap();
|
||||
image.putString("type", mimeType);
|
||||
image.putDouble("fileSize", fileSize);
|
||||
image.putString("fileName", fileName);
|
||||
image.putString("uri", "file://" + uri);
|
||||
|
||||
images = Arguments.createArray();
|
||||
images.pushMap(image);
|
||||
} catch (FileNotFoundException e) {
|
||||
error = Arguments.createMap();
|
||||
error.putString("message", e.getMessage());
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray("data", images);
|
||||
event.putMap("error", error);
|
||||
|
||||
reactContext
|
||||
.getJSModule(RCTEventEmitter.class)
|
||||
.receiveEvent(
|
||||
mEditText.getId(),
|
||||
"onPaste",
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class RNPasteableImageFromUrl implements Runnable {
|
||||
|
||||
private ReactContext mContext;
|
||||
private String mUri;
|
||||
private ReactEditText mTarget;
|
||||
|
||||
RNPasteableImageFromUrl(ReactContext context, ReactEditText target, String uri) {
|
||||
mContext = context;
|
||||
mUri = uri;
|
||||
mTarget = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
WritableArray images = null;
|
||||
WritableMap error = null;
|
||||
|
||||
try {
|
||||
URL url = new URL(mUri);
|
||||
URLConnection u = url.openConnection();
|
||||
|
||||
// Get type
|
||||
String mimeType = u.getHeaderField("Content-Type");
|
||||
if (!mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileSize
|
||||
long fileSize = Long.parseLong(u.getHeaderField("Content-Length"));
|
||||
|
||||
// Get fileName
|
||||
String contentDisposition = u.getHeaderField("Content-Disposition");
|
||||
int startIndex = contentDisposition.indexOf("filename=\"") + 10;
|
||||
int endIndex = contentDisposition.length() - 1;
|
||||
String fileName = contentDisposition.substring(startIndex, endIndex);
|
||||
|
||||
WritableMap image = Arguments.createMap();
|
||||
image.putString("type", mimeType);
|
||||
image.putDouble("fileSize", fileSize);
|
||||
image.putString("fileName", fileName);
|
||||
image.putString("uri", mUri);
|
||||
|
||||
images = Arguments.createArray();
|
||||
images.pushMap(image);
|
||||
|
||||
} catch (IOException e) {
|
||||
error = Arguments.createMap();
|
||||
error.putString("message", e.getMessage());
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray("data", images);
|
||||
event.putMap("error", error);
|
||||
|
||||
mContext
|
||||
.getJSModule(RCTEventEmitter.class)
|
||||
.receiveEvent(
|
||||
mTarget.getId(),
|
||||
"onPaste",
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.os.BuildCompat;
|
||||
import android.text.InputType;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class RNPasteableTextInputManager extends ReactTextInputManager {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PasteableTextInputAndroid";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactEditText createViewInstance(ThemedReactContext context) {
|
||||
RNPasteableEditText editText = new RNPasteableEditText(context) {
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
final InputConnection ic = super.onCreateInputConnection(editorInfo);
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo,
|
||||
new String [] {"image/*"});
|
||||
|
||||
|
||||
final InputConnectionCompat.OnCommitContentListener callback =
|
||||
(inputContentInfo, flags, opts) -> {
|
||||
// read and display inputContentInfo asynchronously
|
||||
if (BuildCompat.isAtLeastNMR1() && (flags &
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
try {
|
||||
inputContentInfo.requestPermission();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.getOnPasteListener().onPaste(inputContentInfo.getContentUri());
|
||||
return true;
|
||||
};
|
||||
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
|
||||
}
|
||||
};
|
||||
int inputType = editText.getInputType();
|
||||
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
|
||||
editText.setReturnKeyType("done");
|
||||
editText.setCustomInsertionActionModeCallback(new RNPasteableActionCallback(editText));
|
||||
editText.setCustomSelectionActionModeCallback(new RNPasteableActionCallback(editText));
|
||||
return editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, ReactEditText editText) {
|
||||
super.addEventEmitters(reactContext, editText);
|
||||
|
||||
RNPasteableEditText pasteableEditText = (RNPasteableEditText)editText;
|
||||
pasteableEditText.setOnPasteListener(new RNPasteableEditTextOnPasteListener(pasteableEditText));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
|
||||
Map map = super.getExportedViewConstants();
|
||||
map.put("onPaste", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onPaste")));
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class RNPasteableTextInputPackage implements ReactPackage {
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
|
||||
return Arrays.asList(
|
||||
new RNPasteableTextInputManager()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.UIBlock;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
public class RNTextInputResetModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private final ReactApplicationContext reactContext;
|
||||
|
||||
public RNTextInputResetModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RNTextInputReset";
|
||||
}
|
||||
|
||||
// https://github.com/facebook/react-native/pull/12462#issuecomment-298812731
|
||||
@ReactMethod
|
||||
public void resetKeyboardInput(final int reactTagToReset) {
|
||||
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
|
||||
uiManager.addUIBlock(new UIBlock() {
|
||||
@Override
|
||||
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
|
||||
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
|
||||
imm.restartInput(viewToReset);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import java.lang.System;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
|
||||
public class ReceiptDelivery {
|
||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
||||
|
||||
public static void send(Context context, final String ackId, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
|
||||
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
String token = map.getString("password");
|
||||
String serverUrl = map.getString("service");
|
||||
if (serverUrl.isEmpty()) {
|
||||
String[] credentials = token.split(",[ ]*");
|
||||
if (credentials.length == 2) {
|
||||
token = credentials[0];
|
||||
serverUrl = credentials[1];
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
|
||||
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
if (token == null) {
|
||||
promise.reject("Receipt delivery failure", "Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverUrl == null) {
|
||||
promise.reject("Receipt delivery failure", "Invalid server URL");
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
long receivedAt = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
json = new JSONObject();
|
||||
json.put("id", ackId);
|
||||
json.put("received_at", receivedAt);
|
||||
json.put("platform", "android");
|
||||
json.put("type", type);
|
||||
json.put("post_id", postId);
|
||||
json.put("is_id_loaded", isIdLoaded);
|
||||
} catch (JSONException e) {
|
||||
Log.e("ReactNative", "Receipt delivery failed to build json payload");
|
||||
promise.reject("Receipt delivery failure", e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
final HttpUrl url = HttpUrl.parse(
|
||||
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
|
||||
if (url != null) {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
RequestBody body = RequestBody.create(JSON, json.toString());
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.url(url)
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
String responseBody = response.body().string();
|
||||
if (response.code() != 200 || !isIdLoaded) {
|
||||
throw new Exception(responseBody);
|
||||
}
|
||||
JSONObject jsonResponse = new JSONObject(responseBody);
|
||||
Bundle bundle = new Bundle();
|
||||
String keys[] = new String[] {"post_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
String key = keys[i];
|
||||
if (jsonResponse.has(key)) {
|
||||
bundle.putString(key, jsonResponse.getString(key));
|
||||
}
|
||||
}
|
||||
promise.resolve(bundle);
|
||||
} catch (Exception e) {
|
||||
Log.e("ReactNative", "Receipt delivery failed to send");
|
||||
promise.reject("Receipt delivery failure", e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
this.clear();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
currentActivity.finishAndRemoveTask();
|
||||
currentActivity.finish();
|
||||
}
|
||||
|
||||
if (data != null && data.hasKey("url")) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<android.support.percent.PercentRelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
@@ -16,4 +16,4 @@
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/splash" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</android.support.percent.PercentRelativeLayout>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<!-- Trust preinstalled CAs -->
|
||||
<certificates src="system" />
|
||||
|
||||
@@ -5,17 +5,15 @@ buildscript {
|
||||
buildToolsVersion = "28.0.3"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 28
|
||||
targetSdkVersion = 26
|
||||
supportLibVersion = "28.0.0"
|
||||
}
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.2'
|
||||
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
|
||||
@@ -39,7 +37,6 @@ subprojects {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
@@ -50,8 +47,5 @@ allprojects {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url "$rootDir/../node_modules/jsc-android/dist"
|
||||
}
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,3 @@ org.gradle.jvmargs=-Xmx2048M
|
||||
|
||||
#android.enableAapt2=false
|
||||
#android.useDeprecatedNdk=true
|
||||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
|
||||
|
||||
8
android/keystores/BUCK
Normal file
@@ -0,0 +1,8 @@
|
||||
keystore(
|
||||
name = "debug",
|
||||
properties = "debug.keystore.properties",
|
||||
store = "debug.keystore",
|
||||
visibility = [
|
||||
"PUBLIC",
|
||||
],
|
||||
)
|
||||
4
android/keystores/debug.keystore.properties
Normal file
@@ -0,0 +1,4 @@
|
||||
key.store=debug.keystore
|
||||
key.alias=androiddebugkey
|
||||
key.store.password=android
|
||||
key.alias.password=android
|
||||
@@ -1,12 +1,4 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':@sentry_react-native'
|
||||
project(':@sentry_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android')
|
||||
include ':react-native-android-open-settings'
|
||||
project(':react-native-android-open-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-open-settings/android')
|
||||
include ':react-native-fast-image'
|
||||
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
|
||||
include ':react-native-haptic-feedback'
|
||||
project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
|
||||
include ':react-native-gesture-handler'
|
||||
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
|
||||
include ':react-native-document-picker'
|
||||
@@ -19,6 +11,8 @@ include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
|
||||
include ':react-native-youtube'
|
||||
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
|
||||
include ':react-native-sentry'
|
||||
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
|
||||
include ':react-native-exception-handler'
|
||||
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
|
||||
include ':rn-fetch-blob'
|
||||
@@ -28,9 +22,11 @@ project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_m
|
||||
include ':react-native-local-auth'
|
||||
project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android')
|
||||
include ':react-native-navigation'
|
||||
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/lib/android/app/')
|
||||
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
|
||||
include ':react-native-image-picker'
|
||||
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
|
||||
include ':react-native-bottom-sheet'
|
||||
project(':react-native-bottom-sheet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bottom-sheet/android')
|
||||
include ':react-native-device-info'
|
||||
project(':react-native-device-info').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-device-info/android')
|
||||
include ':react-native-cookies'
|
||||
@@ -38,16 +34,14 @@ project(':react-native-cookies').projectDir = new File(rootProject.projectDir, '
|
||||
include ':react-native-vector-icons'
|
||||
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
|
||||
include ':reactnativenotifications'
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android/app')
|
||||
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
|
||||
|
||||
include ':app'
|
||||
include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
include ':react-native-linear-gradient'
|
||||
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||
include ':react-native-recyclerview-list'
|
||||
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
include ':@react-native-community_async-storage'
|
||||
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
|
||||
include ':@react-native-community_netinfo'
|
||||
project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import store from 'app/store';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
function getThemeFromState() {
|
||||
const state = store.getState();
|
||||
|
||||
return getTheme(state);
|
||||
}
|
||||
|
||||
export function resetToChannel(passProps = {}) {
|
||||
const theme = getThemeFromState();
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: 'Channel',
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function resetToSelectServer(allowOtherServers) {
|
||||
const theme = getThemeFromState();
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: 'SelectServer',
|
||||
passProps: {
|
||||
allowOtherServers,
|
||||
},
|
||||
options: {
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function resetToTeams(name, title, passProps = {}, options = {}) {
|
||||
const theme = getThemeFromState();
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function goToScreen(name, title, passProps = {}, options = {}) {
|
||||
const theme = getThemeFromState();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.push(componentId, {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function popTopScreen(screenId) {
|
||||
if (screenId) {
|
||||
Navigation.pop(screenId);
|
||||
} else {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
Navigation.pop(componentId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function popToRoot() {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
try {
|
||||
await Navigation.popToRoot(componentId);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there are no screens
|
||||
// atop the root screen to pop. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
|
||||
export function showModal(name, title, passProps = {}, options = {}) {
|
||||
const theme = getThemeFromState();
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.showModal({
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
|
||||
const title = '';
|
||||
const animationsEnabled = (Platform.OS === 'android').toString();
|
||||
const defaultOptions = {
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
animations: {
|
||||
showModal: {
|
||||
enabled: animationsEnabled,
|
||||
alpha: {
|
||||
from: 0,
|
||||
to: 1,
|
||||
duration: 250,
|
||||
},
|
||||
},
|
||||
dismissModal: {
|
||||
enabled: animationsEnabled,
|
||||
alpha: {
|
||||
from: 1,
|
||||
to: 0,
|
||||
duration: 250,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mergeOptions = merge(defaultOptions, options);
|
||||
|
||||
showModal(name, title, passProps, mergeOptions);
|
||||
}
|
||||
|
||||
export function showSearchModal(initialValue = '') {
|
||||
const name = 'Search';
|
||||
const title = '';
|
||||
const passProps = {initialValue};
|
||||
const options = {
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
|
||||
showModal(name, title, passProps, options);
|
||||
}
|
||||
|
||||
export async function dismissModal(options = {}) {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there is no modal to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAllModals(options = {}) {
|
||||
try {
|
||||
await Navigation.dismissAllModals(options);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there are no modals to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
|
||||
export function peek(name, passProps = {}, options = {}) {
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
const defaultOptions = {
|
||||
preview: {
|
||||
commit: false,
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.push(componentId, {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function setButtons(componentId, buttons = {leftButtons: [], rightButtons: []}) {
|
||||
const options = {
|
||||
topBar: {
|
||||
...buttons,
|
||||
},
|
||||
};
|
||||
|
||||
mergeNavigationOptions(componentId, options);
|
||||
}
|
||||
|
||||
export function mergeNavigationOptions(componentId, options) {
|
||||
Navigation.mergeOptions(componentId, options);
|
||||
}
|
||||
|
||||
export function showOverlay(name, passProps, options = {}) {
|
||||
const defaultOptions = {
|
||||
overlay: {
|
||||
interceptTouchOutside: false,
|
||||
},
|
||||
};
|
||||
|
||||
Navigation.showOverlay({
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function dismissOverlay(componentId) {
|
||||
try {
|
||||
await Navigation.dismissOverlay(componentId);
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there is no modal with
|
||||
// this componentId to dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
@@ -1,473 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import * as NavigationActions from 'app/actions/navigation';
|
||||
|
||||
jest.unmock('app/actions/navigation');
|
||||
jest.mock('app/store/ephemeral_store', () => ({
|
||||
getNavigationTopComponentId: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('app/actions/navigation', () => {
|
||||
const topComponentId = 'top-component-id';
|
||||
const name = 'name';
|
||||
const title = 'title';
|
||||
const theme = Preferences.THEMES.default;
|
||||
const passProps = {
|
||||
testProp: 'prop',
|
||||
};
|
||||
const options = {
|
||||
testOption: 'test',
|
||||
};
|
||||
EphemeralStore.getNavigationTopComponentId.mockReturnValue(topComponentId);
|
||||
|
||||
test('resetToChannel should call Navigation.setRoot', () => {
|
||||
const setRoot = jest.spyOn(Navigation, 'setRoot');
|
||||
|
||||
const expectedLayout = {
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: 'Channel',
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.resetToChannel(passProps);
|
||||
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('resetToSelectServer should call Navigation.setRoot', () => {
|
||||
const setRoot = jest.spyOn(Navigation, 'setRoot');
|
||||
|
||||
const allowOtherServers = false;
|
||||
const expectedLayout = {
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: 'SelectServer',
|
||||
passProps: {
|
||||
allowOtherServers,
|
||||
},
|
||||
options: {
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.resetToSelectServer(allowOtherServers);
|
||||
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('resetToTeams should call Navigation.setRoot', () => {
|
||||
const setRoot = jest.spyOn(Navigation, 'setRoot');
|
||||
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
visible: true,
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
root: {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
}],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.resetToTeams(name, title, passProps, options);
|
||||
expect(setRoot).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('goToScreen should call Navigation.push', () => {
|
||||
const push = jest.spyOn(Navigation, 'push');
|
||||
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.goToScreen(name, title, passProps, options);
|
||||
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
|
||||
});
|
||||
|
||||
test('popTopScreen should call Navigation.pop', () => {
|
||||
const pop = jest.spyOn(Navigation, 'pop');
|
||||
|
||||
NavigationActions.popTopScreen();
|
||||
expect(pop).toHaveBeenCalledWith(topComponentId);
|
||||
|
||||
const otherComponentId = `other-${topComponentId}`;
|
||||
NavigationActions.popTopScreen(otherComponentId);
|
||||
expect(pop).toHaveBeenCalledWith(otherComponentId);
|
||||
});
|
||||
|
||||
test('popToRoot should call Navigation.popToRoot', async () => {
|
||||
const popToRoot = jest.spyOn(Navigation, 'popToRoot');
|
||||
|
||||
await NavigationActions.popToRoot();
|
||||
expect(popToRoot).toHaveBeenCalledWith(topComponentId);
|
||||
});
|
||||
|
||||
test('showModal should call Navigation.showModal', () => {
|
||||
const showModal = jest.spyOn(Navigation, 'showModal');
|
||||
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: title,
|
||||
},
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.showModal(name, title, passProps, options);
|
||||
expect(showModal).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('showModalOverCurrentContext should call Navigation.showModal', () => {
|
||||
const showModal = jest.spyOn(Navigation, 'showModal');
|
||||
|
||||
const animationsEnabled = (Platform.OS === 'android').toString();
|
||||
const showModalOverCurrentContextTitle = '';
|
||||
const showModalOverCurrentContextOptions = {
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
animations: {
|
||||
showModal: {
|
||||
enabled: animationsEnabled,
|
||||
alpha: {
|
||||
from: 0,
|
||||
to: 1,
|
||||
duration: 250,
|
||||
},
|
||||
},
|
||||
dismissModal: {
|
||||
enabled: animationsEnabled,
|
||||
alpha: {
|
||||
from: 1,
|
||||
to: 0,
|
||||
duration: 250,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const showModalOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: showModalOverCurrentContextTitle,
|
||||
},
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
const defaultOptions = merge(showModalOverCurrentContextOptions, options);
|
||||
|
||||
const expectedLayout = {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(showModalOptions, defaultOptions),
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.showModalOverCurrentContext(name, passProps, options);
|
||||
expect(showModal).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('showSearchModal should call Navigation.showModal', () => {
|
||||
const showModal = jest.spyOn(Navigation, 'showModal');
|
||||
|
||||
const showSearchModalName = 'Search';
|
||||
const showSearchModalTitle = '';
|
||||
const initialValue = 'initial-value';
|
||||
const showSearchModalPassProps = {initialValue};
|
||||
const showSearchModalOptions = {
|
||||
topBar: {
|
||||
visible: false,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
const defaultOptions = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
statusBar: {
|
||||
visible: true,
|
||||
},
|
||||
topBar: {
|
||||
animate: true,
|
||||
visible: true,
|
||||
backButton: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
title: '',
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
},
|
||||
title: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
text: showSearchModalTitle,
|
||||
},
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
stack: {
|
||||
children: [{
|
||||
component: {
|
||||
name: showSearchModalName,
|
||||
passProps: showSearchModalPassProps,
|
||||
options: merge(defaultOptions, showSearchModalOptions),
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.showSearchModal(initialValue);
|
||||
expect(showModal).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('dismissModal should call Navigation.dismissModal', async () => {
|
||||
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
|
||||
|
||||
await NavigationActions.dismissModal(options);
|
||||
expect(dismissModal).toHaveBeenCalledWith(topComponentId, options);
|
||||
});
|
||||
|
||||
test('dismissAllModals should call Navigation.dismissAllModals', async () => {
|
||||
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
|
||||
|
||||
await NavigationActions.dismissAllModals(options);
|
||||
expect(dismissAllModals).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
test('peek should call Navigation.push', async () => {
|
||||
const push = jest.spyOn(Navigation, 'push');
|
||||
|
||||
const defaultOptions = {
|
||||
preview: {
|
||||
commit: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
};
|
||||
|
||||
await NavigationActions.peek(name, passProps, options);
|
||||
expect(push).toHaveBeenCalledWith(topComponentId, expectedLayout);
|
||||
});
|
||||
|
||||
test('mergeNavigationOptions should call Navigation.mergeOptions', () => {
|
||||
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
|
||||
|
||||
NavigationActions.mergeNavigationOptions(topComponentId, options);
|
||||
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, options);
|
||||
});
|
||||
|
||||
test('setButtons should call Navigation.mergeOptions', () => {
|
||||
const mergeOptions = jest.spyOn(Navigation, 'mergeOptions');
|
||||
|
||||
const buttons = {
|
||||
leftButtons: ['left-button'],
|
||||
rightButtons: ['right-button'],
|
||||
};
|
||||
const setButtonsOptions = {
|
||||
topBar: {
|
||||
...buttons,
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.setButtons(topComponentId, buttons);
|
||||
expect(mergeOptions).toHaveBeenCalledWith(topComponentId, setButtonsOptions);
|
||||
});
|
||||
|
||||
test('showOverlay should call Navigation.showOverlay', () => {
|
||||
const showOverlay = jest.spyOn(Navigation, 'showOverlay');
|
||||
|
||||
const defaultOptions = {
|
||||
overlay: {
|
||||
interceptTouchOutside: false,
|
||||
},
|
||||
};
|
||||
|
||||
const expectedLayout = {
|
||||
component: {
|
||||
name,
|
||||
passProps,
|
||||
options: merge(defaultOptions, options),
|
||||
},
|
||||
};
|
||||
|
||||
NavigationActions.showOverlay(name, passProps, options);
|
||||
expect(showOverlay).toHaveBeenCalledWith(expectedLayout);
|
||||
});
|
||||
|
||||
test('dismissOverlay should call Navigation.dismissOverlay', async () => {
|
||||
const dismissOverlay = jest.spyOn(Navigation, 'dismissOverlay');
|
||||
|
||||
await NavigationActions.dismissOverlay(topComponentId);
|
||||
expect(dismissOverlay).toHaveBeenCalledWith(topComponentId);
|
||||
});
|
||||
});
|
||||
@@ -10,42 +10,24 @@ import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
markChannelAsRead,
|
||||
markChannelAsViewed,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
|
||||
selectChannel,
|
||||
getChannelStats,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {
|
||||
getPosts,
|
||||
getPostsBefore,
|
||||
getPostsSince,
|
||||
getPostThread,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getTeamMembersByIds, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {
|
||||
getChannel,
|
||||
getCurrentChannelId,
|
||||
getMyChannelMember,
|
||||
getRedirectChannelNameForTeam,
|
||||
getChannelsNameMapInTeam,
|
||||
isManuallyUnread,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getChannel, getCurrentChannelId, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
import {
|
||||
getChannelByName,
|
||||
getDirectChannelName,
|
||||
getUserIdFromChannelName,
|
||||
isDirectChannel,
|
||||
isGroupChannel,
|
||||
getChannelByName as getChannelByNameSelector,
|
||||
} from 'mattermost-redux/utils/channel_utils';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
|
||||
@@ -57,8 +39,8 @@ import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels'
|
||||
const MAX_POST_TRIES = 3;
|
||||
|
||||
export function loadChannelsIfNecessary(teamId) {
|
||||
return async (dispatch) => {
|
||||
await dispatch(fetchMyChannelsAndMembers(teamId));
|
||||
return async (dispatch, getState) => {
|
||||
await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,7 +82,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
const directChannels = Object.values(channels).filter((c) => (isDirectChannel(c) || isGroupChannel(c)));
|
||||
directChannels.forEach((channel) => {
|
||||
const member = myMembers[channel.id];
|
||||
if (isDirectChannel(channel) && !isDirectChannelVisible(currentUserId, myPreferences, channel) && member && member.mention_count > 0) {
|
||||
if (isDirectChannel(channel) && !isDirectChannelVisible(currentUserId, myPreferences, channel) && member.mention_count > 0) {
|
||||
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
let pref = dmPrefs.get(teammateId);
|
||||
if (pref) {
|
||||
@@ -110,7 +92,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
}
|
||||
dmPrefs.set(teammateId, pref);
|
||||
prefs.push(pref);
|
||||
} else if (isGroupChannel(channel) && !isGroupChannelVisible(myPreferences, channel) && member && (member.mention_count > 0 || member.msg_count < channel.total_msg_count)) {
|
||||
} else if (isGroupChannel(channel) && !isGroupChannelVisible(myPreferences, channel) && (member.mention_count > 0 || member.msg_count < channel.total_msg_count)) {
|
||||
const id = channel.id;
|
||||
let pref = gmPrefs.get(id);
|
||||
if (pref) {
|
||||
@@ -177,8 +159,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts} = state.entities.posts;
|
||||
const postsIds = getPostIdsInChannel(state, channelId);
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
const postsIds = postsInChannel[channelId];
|
||||
const actions = [];
|
||||
|
||||
const time = Date.now();
|
||||
@@ -201,7 +183,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const lastConnectAt = state.websocket?.lastConnectAt || 0;
|
||||
const {lastConnectAt} = state.device.websocket;
|
||||
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
|
||||
|
||||
let since;
|
||||
@@ -268,14 +250,14 @@ export function loadFilesForPostIfNecessary(postId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadThreadIfNecessary(rootId) {
|
||||
return (dispatch, getState) => {
|
||||
export function loadThreadIfNecessary(rootId, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts, postsInThread} = state.entities.posts;
|
||||
const threadPosts = postsInThread[rootId];
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
const channelPosts = postsInChannel[channelId];
|
||||
|
||||
if (!posts[rootId] || !threadPosts) {
|
||||
dispatch(getPostThread(rootId));
|
||||
if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) {
|
||||
getPostThread(rootId, false)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -342,20 +324,19 @@ export function selectPenultimateChannel(teamId) {
|
||||
|
||||
export function selectDefaultChannel(teamId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const channel = getChannelByNameSelector(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
const {channels} = getState().entities.channels;
|
||||
|
||||
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
|
||||
let channelId;
|
||||
if (channel) {
|
||||
channelId = channel.id;
|
||||
} else {
|
||||
// Handle case when the default channel cannot be found
|
||||
// so we need to get the first available channel of the team
|
||||
const channels = Object.values(channelsInTeam);
|
||||
const firstChannel = channels.length ? channels[0].id : '';
|
||||
channelId = firstChannel;
|
||||
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
|
||||
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
|
||||
|
||||
channelId = firstChannel.id;
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
@@ -380,23 +361,32 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
}
|
||||
|
||||
let previousChannelId;
|
||||
dispatch(batchActions([
|
||||
selectChannel(channelId),
|
||||
setChannelDisplayName(channel.display_name),
|
||||
{
|
||||
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
},
|
||||
setChannelLoading(false),
|
||||
{
|
||||
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
|
||||
teamId: currentTeamId,
|
||||
channelId,
|
||||
},
|
||||
{
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
member,
|
||||
},
|
||||
]));
|
||||
|
||||
let markPreviousChannelId;
|
||||
if (!fromPushNotification && !sameChannel) {
|
||||
previousChannelId = currentChannelId;
|
||||
markPreviousChannelId = currentChannelId;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
selectChannel(channelId),
|
||||
getChannelStats(channelId),
|
||||
setChannelDisplayName(channel.display_name),
|
||||
setInitialPostVisibility(channelId),
|
||||
setChannelLoading(false),
|
||||
setLastChannelForTeam(currentTeamId, channelId),
|
||||
selectChannelWithMember(channelId, channel, member),
|
||||
];
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(markChannelViewedAndRead(channelId, previousChannelId));
|
||||
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -404,16 +394,9 @@ export function handleSelectChannelByName(channelName, teamName) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
const currentTeam = currentTeams[currentTeamId];
|
||||
const currentTeamName = currentTeam?.name;
|
||||
const currentTeamName = currentTeams[currentTeamId]?.name;
|
||||
const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
if (teamName && teamName !== currentTeamName) {
|
||||
const team = getTeamByName(state, teamName);
|
||||
dispatch(selectTeam(team));
|
||||
}
|
||||
|
||||
if (channel && currentChannelId !== channel.id) {
|
||||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
@@ -448,17 +431,6 @@ export function markChannelViewedAndRead(channelId, previousChannelId, markOnSer
|
||||
};
|
||||
}
|
||||
|
||||
export function markChannelViewedAndReadOnReconnect(channelId) {
|
||||
return (dispatch, getState) => {
|
||||
if (isManuallyUnread(getState(), channelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(markChannelAsRead(channelId));
|
||||
dispatch(markChannelAsViewed(channelId));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleDMChannel(otherUserId, visible, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -552,12 +524,6 @@ export function leaveChannel(channel, reset = false) {
|
||||
}
|
||||
|
||||
export function setChannelLoading(loading = true) {
|
||||
if (loading) {
|
||||
telemetry.start(['channel:loading']);
|
||||
} else {
|
||||
telemetry.end(['channel:loading']);
|
||||
}
|
||||
|
||||
return {
|
||||
type: ViewTypes.SET_CHANNEL_LOADER,
|
||||
loading,
|
||||
@@ -586,7 +552,7 @@ export function setChannelDisplayName(displayName) {
|
||||
}
|
||||
|
||||
// Returns true if there are more posts to load
|
||||
export function increasePostVisibility(channelId, postId) {
|
||||
export function increasePostVisibility(channelId, focusedPostId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {loadingPosts, postVisibility} = state.views.channel;
|
||||
@@ -596,28 +562,22 @@ export function increasePostVisibility(channelId, postId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!postId) {
|
||||
// No posts are visible, so the channel is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we already have the posts that we want to show
|
||||
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
|
||||
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
if (!focusedPostId) {
|
||||
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
|
||||
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
|
||||
if (loadedPostCount >= desiredPostVisibility) {
|
||||
// We already have the posts, so we just need to show them
|
||||
dispatch(batchActions([
|
||||
doIncreasePostVisibility(channelId),
|
||||
setLoadMorePostsVisible(true),
|
||||
]));
|
||||
if (loadedPostCount >= desiredPostVisibility) {
|
||||
// We already have the posts, so we just need to show them
|
||||
dispatch(batchActions([
|
||||
doIncreasePostVisibility(channelId),
|
||||
setLoadMorePostsVisible(true),
|
||||
]));
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.start(['posts:loading']);
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: true,
|
||||
@@ -625,8 +585,14 @@ export function increasePostVisibility(channelId, postId) {
|
||||
});
|
||||
|
||||
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
const page = Math.floor(currentPostVisibility / pageSize);
|
||||
|
||||
const result = await retryGetPostsAction(getPostsBefore(channelId, postId, 0, pageSize), dispatch, getState);
|
||||
let result;
|
||||
if (focusedPostId) {
|
||||
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
|
||||
} else {
|
||||
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
|
||||
}
|
||||
|
||||
const actions = [{
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
@@ -655,9 +621,6 @@ export function increasePostVisibility(channelId, postId) {
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
telemetry.end(['posts:loading']);
|
||||
telemetry.save();
|
||||
|
||||
return hasMorePost;
|
||||
};
|
||||
}
|
||||
@@ -676,27 +639,3 @@ function setLoadMorePostsVisible(visible) {
|
||||
data: visible,
|
||||
};
|
||||
}
|
||||
|
||||
function setInitialPostVisibility(channelId) {
|
||||
return {
|
||||
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
};
|
||||
}
|
||||
|
||||
function setLastChannelForTeam(teamId, channelId) {
|
||||
return {
|
||||
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
|
||||
teamId,
|
||||
channelId,
|
||||
};
|
||||
}
|
||||
|
||||
function selectChannelWithMember(channelId, channel, member) {
|
||||
return {
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
channel,
|
||||
member,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,38 +4,13 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import initialState from 'app/initial_state';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import testHelper from 'test/test_helper';
|
||||
import {handleSelectChannelByName} from 'app/actions/views/channel';
|
||||
|
||||
import * as ChannelActions from 'app/actions/views/channel';
|
||||
const {
|
||||
handleSelectChannel,
|
||||
handleSelectChannelByName,
|
||||
loadPostsIfNecessaryWithRetry,
|
||||
} = ChannelActions;
|
||||
|
||||
import postReducer from 'mattermost-redux/reducers/entities/posts';
|
||||
|
||||
const MOCK_CHANNEL_MARK_AS_READ = 'MOCK_CHANNEL_MARK_AS_READ';
|
||||
const MOCK_CHANNEL_MARK_AS_VIEWED = 'MOCK_CHANNEL_MARK_AS_VIEWED';
|
||||
|
||||
jest.mock('mattermost-redux/actions/channels', () => {
|
||||
const channelActions = require.requireActual('mattermost-redux/actions/channels');
|
||||
return {
|
||||
...channelActions,
|
||||
markChannelAsRead: jest.fn().mockReturnValue({type: 'MOCK_CHANNEL_MARK_AS_READ'}),
|
||||
markChannelAsViewed: jest.fn().mockReturnValue({type: 'MOCK_CHANNEL_MARK_AS_VIEWED'}),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/teams', () => {
|
||||
const teamSelectors = require.requireActual('mattermost-redux/selectors/entities/teams');
|
||||
return {
|
||||
...teamSelectors,
|
||||
getTeamByName: jest.fn(() => ({name: 'current-team-name'})),
|
||||
};
|
||||
});
|
||||
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
|
||||
getChannel: () => ({data: 'received-channel-id'}),
|
||||
getCurrentChannelId: () => 'current-channel-id',
|
||||
getMyChannelMember: () => ({data: {member: {}}}),
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
@@ -44,9 +19,6 @@ describe('Actions.Views.Channel', () => {
|
||||
|
||||
const MOCK_SELECT_CHANNEL_TYPE = 'MOCK_SELECT_CHANNEL_TYPE';
|
||||
const MOCK_RECEIVE_CHANNEL_TYPE = 'MOCK_RECEIVE_CHANNEL_TYPE';
|
||||
const MOCK_RECEIVED_POSTS = 'RECEIVED_POSTS';
|
||||
const MOCK_RECEIVED_POSTS_IN_CHANNEL = 'RECEIVED_POSTS_IN_CHANNEL';
|
||||
const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE';
|
||||
|
||||
const actions = require('mattermost-redux/actions/channels');
|
||||
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
|
||||
@@ -66,51 +38,14 @@ describe('Actions.Views.Channel', () => {
|
||||
type: MOCK_SELECT_CHANNEL_TYPE,
|
||||
data: 'selected-channel-id',
|
||||
});
|
||||
const postActions = require('mattermost-redux/actions/posts');
|
||||
postActions.getPostsSince = jest.fn(() => {
|
||||
return {
|
||||
type: MOCK_RECEIVED_POSTS_SINCE,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
postActions.getPosts = jest.fn((channelId) => {
|
||||
const order = [];
|
||||
const posts = {};
|
||||
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const p = testHelper.fakePost(channelId);
|
||||
order.push(p.id);
|
||||
posts[p.id] = p;
|
||||
}
|
||||
|
||||
return {
|
||||
type: MOCK_RECEIVED_POSTS,
|
||||
data: {
|
||||
order,
|
||||
posts,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const postUtils = require('mattermost-redux/utils/post_utils');
|
||||
postUtils.getLastCreateAt = jest.fn((array) => {
|
||||
return array[0].create_at;
|
||||
});
|
||||
|
||||
let nextPostState = {};
|
||||
const currentUserId = 'current-user-id';
|
||||
const currentChannelId = 'channel-id';
|
||||
const currentChannelName = 'channel-name';
|
||||
const currentTeamId = 'current-team-id';
|
||||
const currentTeamName = 'current-team-name';
|
||||
const storeObj = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
users: {
|
||||
currentUserId,
|
||||
},
|
||||
@@ -130,11 +65,6 @@ describe('Actions.Views.Channel', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const channelSelectors = require('mattermost-redux/selectors/entities/channels');
|
||||
channelSelectors.getChannel = jest.fn((state, channelId) => ({data: channelId}));
|
||||
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
|
||||
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
|
||||
|
||||
test('handleSelectChannelByName success', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
@@ -163,118 +93,4 @@ describe('Actions.Views.Channel', () => {
|
||||
const storeBatchActions = storeActions.some(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
expect(storeBatchActions).toBe(false);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
expect(postActions.getPosts).toBeCalled();
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const receivedPosts = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS);
|
||||
const receivedPostsAtAction = storeBatchActions[0].payload.some((action) => action.type === 'RECEIVED_POSTS_FOR_CHANNEL_AT_TIME');
|
||||
|
||||
nextPostState = postReducer(store.getState().entities.posts, receivedPosts);
|
||||
nextPostState = postReducer(nextPostState, {
|
||||
type: MOCK_RECEIVED_POSTS_IN_CHANNEL,
|
||||
channelId: currentChannelId,
|
||||
data: receivedPosts.data,
|
||||
recent: true,
|
||||
});
|
||||
|
||||
expect(receivedPostsAtAction).toBe(true);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry get posts since', async () => {
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
entities: {
|
||||
...storeObj.entities,
|
||||
posts: nextPostState,
|
||||
},
|
||||
views: {
|
||||
...storeObj.views,
|
||||
channel: {
|
||||
...storeObj.views.channel,
|
||||
lastGetPosts: {
|
||||
[currentChannelId]: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
const storeActions = store.getActions();
|
||||
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
|
||||
|
||||
expect(postUtils.getLastCreateAt).toBeCalled();
|
||||
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, Object.values(store.getState().entities.posts.posts)[0].create_at);
|
||||
expect(receivedPostsSince).not.toBe(null);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry get posts since the websocket reconnected', async () => {
|
||||
const time = Date.now();
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
entities: {
|
||||
...storeObj.entities,
|
||||
posts: nextPostState,
|
||||
},
|
||||
views: {
|
||||
...storeObj.views,
|
||||
channel: {
|
||||
...storeObj.views.channel,
|
||||
lastGetPosts: {
|
||||
[currentChannelId]: time,
|
||||
},
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
lastConnectAt: time + (1 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(loadPostsIfNecessaryWithRetry(currentChannelId));
|
||||
const storeActions = store.getActions();
|
||||
const receivedPostsSince = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS_SINCE);
|
||||
|
||||
expect(postUtils.getLastCreateAt).not.toBeCalled();
|
||||
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, store.getState().views.channel.lastGetPosts[currentChannelId]);
|
||||
expect(receivedPostsSince).not.toBe(null);
|
||||
});
|
||||
|
||||
const handleSelectChannelCases = [
|
||||
[currentChannelId, true],
|
||||
[currentChannelId, false],
|
||||
[`not-${currentChannelId}`, true],
|
||||
[`not-${currentChannelId}`, false],
|
||||
];
|
||||
test.each(handleSelectChannelCases)('handleSelectChannel dispatches selectChannelWithMember', async (channelId, fromPushNotification) => {
|
||||
store = mockStore({...storeObj});
|
||||
|
||||
await store.dispatch(handleSelectChannel(channelId, fromPushNotification));
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const selectChannelWithMember = storeBatchActions.payload.find(({type}) => type === ViewTypes.SELECT_CHANNEL_WITH_MEMBER);
|
||||
const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED);
|
||||
const readAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_READ);
|
||||
|
||||
const expectedSelectChannelWithMember = {
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
channel: {
|
||||
data: channelId,
|
||||
},
|
||||
member: {
|
||||
data: {
|
||||
member: {},
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember);
|
||||
expect(viewedAction).not.toBe(null);
|
||||
expect(readAction).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {addChannelMember} from 'mattermost-redux/actions/channels';
|
||||
|
||||
export function handleAddChannelMembers(channelId, members) {
|
||||
return async (dispatch) => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const requests = members.map((m) => dispatch(addChannelMember(channelId, m)));
|
||||
const requests = members.map((m) => dispatch(addChannelMember(channelId, m, getState)));
|
||||
|
||||
return await Promise.all(requests);
|
||||
} catch (error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export function executeCommand(message, channelId, rootId) {
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data?.trigger_id) { //eslint-disable-line camelcase
|
||||
if (data.trigger_id) {
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {getSessions} from 'mattermost-redux/actions/users';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import {app} from 'app/mattermost';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
@@ -47,18 +44,18 @@ export function handleSuccessfulLogin() {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
await setCSRFFromCookie(url);
|
||||
setAppCredentials(deviceToken, currentUserId, token, url);
|
||||
app.setAppCredentials(deviceToken, currentUserId, token, url);
|
||||
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
if (enableTimezone) {
|
||||
const timezone = await getDeviceTimezoneAsync();
|
||||
dispatch(autoUpdateTimezone(timezone));
|
||||
dispatch(autoUpdateTimezone(getDeviceTimezone()));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
|
||||
data: {
|
||||
url,
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,7 +75,10 @@ export function scheduleExpiredNotification(intl) {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {deviceToken} = state.entities.general;
|
||||
const config = getConfig(state);
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
|
||||
});
|
||||
|
||||
// Once the user logs in we are going to wait for 10 seconds
|
||||
// before retrieving the session that belongs to this device
|
||||
@@ -99,15 +99,6 @@ export function scheduleExpiredNotification(intl) {
|
||||
|
||||
const session = sessions.data.find((s) => s.device_id === deviceToken);
|
||||
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
|
||||
const expiresInDays = parseInt(Math.ceil(Math.abs(moment.duration(moment().diff(expiresAt)).asDays())), 10);
|
||||
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
|
||||
}, {
|
||||
siteName: config.SiteName,
|
||||
daysCount: expiresInDays,
|
||||
});
|
||||
|
||||
if (expiresAt) {
|
||||
PushNotifications.localNotificationSchedule({
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
handlePasswordChanged,
|
||||
} from 'app/actions/views/login';
|
||||
|
||||
jest.mock('app/init/credentials', () => ({
|
||||
setAppCredentials: () => jest.fn(),
|
||||
jest.mock('app/mattermost', () => ({
|
||||
app: {
|
||||
setAppCredentials: () => jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-native-cookies', () => ({
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {doPostAction, receivedNewPost} from 'mattermost-redux/actions/posts';
|
||||
import {PostTypes} from 'mattermost-redux/action_types';
|
||||
import {doPostAction} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -27,7 +28,16 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
},
|
||||
};
|
||||
|
||||
dispatch(receivedNewPost(post));
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {
|
||||
[post.id]: post,
|
||||
},
|
||||
},
|
||||
channelId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
|
||||
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {receivedNewPost} from 'mattermost-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
@@ -47,7 +46,7 @@ export function loadConfigAndLicense() {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFromPushNotification(notification) {
|
||||
export function loadFromPushNotification(notification, startAppFromPushNotification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {data} = notification;
|
||||
@@ -84,7 +83,7 @@ export function loadFromPushNotification(notification) {
|
||||
dispatch(selectTeam({id: teamId}));
|
||||
}
|
||||
|
||||
dispatch(handleSelectChannel(channelId, true));
|
||||
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +110,16 @@ export function createPostForNotificationReply(post) {
|
||||
|
||||
try {
|
||||
const data = await Client4.createPost({...newPost, create_at: 0});
|
||||
dispatch(receivedNewPost(data));
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {
|
||||
[data.id]: data,
|
||||
},
|
||||
},
|
||||
channelId: data.channel_id,
|
||||
});
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleSearchDraftChanged(text) {
|
||||
@@ -11,3 +13,25 @@ export function handleSearchDraftChanged(text) {
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function showSearchModal(navigator, initialValue = '') {
|
||||
return (dispatch, getState) => {
|
||||
const theme = getTheme(getState());
|
||||
|
||||
const options = {
|
||||
screen: 'Search',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
passProps: {
|
||||
initialValue,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.showModal(options);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ export function handleServerUrlChanged(serverUrl) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setServerUrl(serverUrl) {
|
||||
return {type: ViewTypes.SERVER_URL_CHANGED, serverUrl};
|
||||
}
|
||||
|
||||
export default {
|
||||
handleServerUrlChanged,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
export function userTyping(channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {websocket} = getState();
|
||||
const {websocket} = getState().device;
|
||||
if (websocket.connected) {
|
||||
wsUserTyping(channelId, rootId)(dispatch, getState);
|
||||
}
|
||||
|
||||
321
app/app.js
Normal file
@@ -0,0 +1,321 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable global-require*/
|
||||
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
|
||||
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
|
||||
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {setDeepLinkURL} from 'app/actions/views/root';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
|
||||
import {getTranslations as getLocalTranslations} from 'app/i18n';
|
||||
import {store, handleManagedConfig} from 'app/mattermost';
|
||||
import avoidNativeBridge from 'app/utils/avoid_native_bridge';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
|
||||
const {Initialization} = NativeModules;
|
||||
|
||||
const TOOLBAR_BACKGROUND = 'TOOLBAR_BACKGROUND';
|
||||
const TOOLBAR_TEXT_COLOR = 'TOOLBAR_TEXT_COLOR';
|
||||
const APP_BACKGROUND = 'APP_BACKGROUND';
|
||||
|
||||
export default class App {
|
||||
constructor() {
|
||||
// Usage: app.js
|
||||
this.shouldRelaunchWhenActive = false;
|
||||
this.inBackgroundSince = null;
|
||||
|
||||
// Usage: screen/entry.js
|
||||
this.startAppFromPushNotification = false;
|
||||
this.isNotificationsConfigured = false;
|
||||
this.allowOtherServers = true;
|
||||
this.appStarted = false;
|
||||
this.emmEnabled = false;
|
||||
this.performingEMMAuthentication = false;
|
||||
this.translations = null;
|
||||
this.toolbarBackground = null;
|
||||
this.toolbarTextColor = null;
|
||||
this.appBackground = null;
|
||||
|
||||
// Usage utils/push_notifications.js
|
||||
this.replyNotificationData = null;
|
||||
this.deviceToken = null;
|
||||
|
||||
// Usage credentials
|
||||
this.currentUserId = null;
|
||||
this.token = null;
|
||||
this.url = null;
|
||||
|
||||
// Load polyfill for iOS 9
|
||||
if (Platform.OS === 'ios') {
|
||||
const majorVersionIOS = parseInt(Platform.Version, 10);
|
||||
if (majorVersionIOS < 10) {
|
||||
require('@babel/polyfill');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage deeplinking
|
||||
Linking.addEventListener('url', this.handleDeepLink);
|
||||
|
||||
this.setFontFamily();
|
||||
this.getStartupThemes();
|
||||
this.getAppCredentials();
|
||||
}
|
||||
|
||||
setFontFamily = () => {
|
||||
// Set a global font for Android
|
||||
if (Platform.OS === 'android') {
|
||||
const defaultFontFamily = {
|
||||
style: {
|
||||
fontFamily: 'Roboto',
|
||||
},
|
||||
};
|
||||
const TextRender = Text.render;
|
||||
const initialDefaultProps = Text.defaultProps;
|
||||
Text.defaultProps = {
|
||||
...initialDefaultProps,
|
||||
...defaultFontFamily,
|
||||
};
|
||||
Text.render = function render(props, ...args) {
|
||||
const oldProps = props;
|
||||
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
|
||||
try {
|
||||
return Reflect.apply(TextRender, this, [newProps, ...args]);
|
||||
} finally {
|
||||
newProps = oldProps;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getTranslations = () => {
|
||||
if (this.translations) {
|
||||
return this.translations;
|
||||
}
|
||||
|
||||
const state = store.getState();
|
||||
const locale = getCurrentLocale(state);
|
||||
|
||||
this.translations = getLocalTranslations(locale);
|
||||
return this.translations;
|
||||
};
|
||||
|
||||
getAppCredentials = async () => {
|
||||
try {
|
||||
const credentials = await avoidNativeBridge(
|
||||
() => {
|
||||
return Initialization.credentialsExist;
|
||||
},
|
||||
() => {
|
||||
return Initialization.credentials;
|
||||
},
|
||||
() => {
|
||||
this.waitForRehydration = true;
|
||||
return getGenericPassword();
|
||||
}
|
||||
);
|
||||
|
||||
if (credentials) {
|
||||
const usernameParsed = credentials.username.split(',');
|
||||
const passwordParsed = credentials.password.split(',');
|
||||
|
||||
// username == deviceToken, currentUserId
|
||||
// password == token, url
|
||||
if (usernameParsed.length === 2 && passwordParsed.length === 2) {
|
||||
const [deviceToken, currentUserId] = usernameParsed;
|
||||
const [token, url] = passwordParsed;
|
||||
|
||||
// if for any case the url and the token aren't valid proceed with re-hydration
|
||||
if (url && url !== 'undefined' && token && token !== 'undefined') {
|
||||
this.deviceToken = deviceToken;
|
||||
this.currentUserId = currentUserId;
|
||||
this.token = token;
|
||||
this.url = url;
|
||||
Client4.setUrl(url);
|
||||
Client4.setToken(token);
|
||||
await setCSRFFromCookie(url);
|
||||
} else {
|
||||
this.waitForRehydration = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.waitForRehydration = false;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getStartupThemes = async () => {
|
||||
try {
|
||||
const [
|
||||
toolbarBackground,
|
||||
toolbarTextColor,
|
||||
appBackground,
|
||||
] = await avoidNativeBridge(
|
||||
() => {
|
||||
return Initialization.themesExist;
|
||||
},
|
||||
() => {
|
||||
return [
|
||||
Initialization.toolbarBackground,
|
||||
Initialization.toolbarTextColor,
|
||||
Initialization.appBackground,
|
||||
];
|
||||
},
|
||||
() => {
|
||||
return Promise.all([
|
||||
AsyncStorage.getItem(TOOLBAR_BACKGROUND),
|
||||
AsyncStorage.getItem(TOOLBAR_TEXT_COLOR),
|
||||
AsyncStorage.getItem(APP_BACKGROUND),
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
if (toolbarBackground) {
|
||||
this.toolbarBackground = toolbarBackground;
|
||||
this.toolbarTextColor = toolbarTextColor;
|
||||
this.appBackground = appBackground;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
setPerformingEMMAuthentication = (authenticating) => {
|
||||
this.performingEMMAuthentication = authenticating;
|
||||
};
|
||||
|
||||
setAppCredentials = (deviceToken, currentUserId, token, url) => {
|
||||
if (!currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = `${deviceToken}, ${currentUserId}`;
|
||||
const password = `${token},${url}`;
|
||||
|
||||
if (this.waitForRehydration) {
|
||||
this.waitForRehydration = false;
|
||||
this.token = token;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
// Only save to keychain if the url and token are set
|
||||
if (url && token) {
|
||||
try {
|
||||
setGenericPassword(username, password);
|
||||
} catch (e) {
|
||||
console.warn('could not set credentials', e); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
|
||||
AsyncStorage.setItem(TOOLBAR_BACKGROUND, toolbarBackground);
|
||||
AsyncStorage.setItem(TOOLBAR_TEXT_COLOR, toolbarTextColor);
|
||||
AsyncStorage.setItem(APP_BACKGROUND, appBackground);
|
||||
};
|
||||
|
||||
setStartAppFromPushNotification = (startAppFromPushNotification) => {
|
||||
this.startAppFromPushNotification = startAppFromPushNotification;
|
||||
};
|
||||
|
||||
setIsNotificationsConfigured = (isNotificationsConfigured) => {
|
||||
this.isNotificationsConfigured = isNotificationsConfigured;
|
||||
};
|
||||
|
||||
setAllowOtherServers = (allowOtherServers) => {
|
||||
this.allowOtherServers = allowOtherServers;
|
||||
};
|
||||
|
||||
setAppStarted = (appStarted) => {
|
||||
this.appStarted = appStarted;
|
||||
};
|
||||
|
||||
setEMMEnabled = (emmEnabled) => {
|
||||
this.emmEnabled = emmEnabled;
|
||||
};
|
||||
|
||||
setDeviceToken = (deviceToken) => {
|
||||
this.deviceToken = deviceToken;
|
||||
};
|
||||
|
||||
setReplyNotificationData = (replyNotificationData) => {
|
||||
this.replyNotificationData = replyNotificationData;
|
||||
};
|
||||
|
||||
setInBackgroundSince = (inBackgroundSince) => {
|
||||
this.inBackgroundSince = inBackgroundSince;
|
||||
};
|
||||
|
||||
setShouldRelaunchWhenActive = (shouldRelaunchWhenActive) => {
|
||||
this.shouldRelaunchWhenActive = shouldRelaunchWhenActive;
|
||||
};
|
||||
|
||||
clearNativeCache = () => {
|
||||
resetGenericPassword();
|
||||
AsyncStorage.multiRemove([
|
||||
TOOLBAR_BACKGROUND,
|
||||
TOOLBAR_TEXT_COLOR,
|
||||
APP_BACKGROUND,
|
||||
]);
|
||||
};
|
||||
|
||||
handleDeepLink = (event) => {
|
||||
const {url} = event;
|
||||
store.dispatch(setDeepLinkURL(url));
|
||||
}
|
||||
|
||||
launchApp = async () => {
|
||||
const shouldStart = await handleManagedConfig();
|
||||
if (shouldStart) {
|
||||
this.startApp();
|
||||
}
|
||||
};
|
||||
|
||||
startApp = () => {
|
||||
if (this.appStarted || this.waitForRehydration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {dispatch} = store;
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
dispatch(setDeepLinkURL(url));
|
||||
});
|
||||
|
||||
let screen = 'SelectServer';
|
||||
if (this.token && this.url) {
|
||||
screen = 'Channel';
|
||||
tracker.initialLoad = Date.now();
|
||||
|
||||
try {
|
||||
dispatch(loadMe());
|
||||
} catch (e) {
|
||||
// Fall through since we should have a previous version of the current user because we have a token
|
||||
console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
switch (screen) {
|
||||
case 'SelectServer':
|
||||
EventEmitter.emit(ViewTypes.LAUNCH_LOGIN, true);
|
||||
break;
|
||||
case 'Channel':
|
||||
EventEmitter.emit(ViewTypes.LAUNCH_CHANNEL, true);
|
||||
break;
|
||||
}
|
||||
|
||||
this.setAppStarted(true);
|
||||
}
|
||||
}
|
||||
6
app/components/__mocks__/react.js
vendored
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
module.exports = {...React, memo: (x) => x};
|
||||
@@ -1,7 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AttachmentButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
@@ -11,7 +12,6 @@ exports[`AttachmentButton should match snapshot 1`] = `
|
||||
"width": 45,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
@@ -24,5 +24,5 @@ exports[`AttachmentButton should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
</TouchableOpacity>
|
||||
`;
|
||||
@@ -5,75 +5,63 @@ exports[`Badge should match snapshot 1`] = `
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#444",
|
||||
"borderRadius": 20,
|
||||
"height": 20,
|
||||
"padding": 12,
|
||||
"paddingBottom": 3,
|
||||
"paddingTop": 3,
|
||||
"position": "absolute",
|
||||
"right": 30,
|
||||
"top": 2,
|
||||
},
|
||||
Object {
|
||||
"borderColor": "#000000",
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
Object {
|
||||
"opacity": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#444",
|
||||
"height": 20,
|
||||
"padding": 12,
|
||||
"paddingBottom": 3,
|
||||
"paddingTop": 3,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
Object {
|
||||
"opacity": 0,
|
||||
},
|
||||
]
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"textAlignVertical": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
<Text
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"textAlignVertical": "center",
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 14,
|
||||
},
|
||||
Object {
|
||||
"color": "#145dbf",
|
||||
"fontSize": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 14,
|
||||
},
|
||||
Object {
|
||||
"color": "#145dbf",
|
||||
"fontSize": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
99+
|
||||
</Text>
|
||||
</View>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Fade should render {opacity: 0} 1`] = `
|
||||
<AnimatedComponent
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"opacity": 0,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"scale": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
text
|
||||
</Text>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
|
||||
exports[`Fade should render {opacity: 1} 1`] = `
|
||||
<AnimatedComponent
|
||||
pointerEvents="box-none"
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"scale": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text>
|
||||
text
|
||||
</Text>
|
||||
</AnimatedComponent>
|
||||
`;
|
||||
@@ -16,6 +16,14 @@ exports[`profile_picture_button should match snapshot 1`] = `
|
||||
}
|
||||
maxFileCount={5}
|
||||
maxFileSize={20971520}
|
||||
navigator={
|
||||
Object {
|
||||
"dismissModal": [MockFunction],
|
||||
"push": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
@@ -27,7 +35,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 3,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "rgba(22,109,224,0.3)",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`SendButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 3,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
}
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
|
||||
exports[`SendButton should render theme backgroundColor 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 3,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 18,
|
||||
"height": 28,
|
||||
"justifyContent": "center",
|
||||
"width": 28,
|
||||
}
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
@@ -1,38 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Swiper should match snapshot 1`] = `
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"flex": 1,
|
||||
"position": "relative",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ScrollViewMock
|
||||
automaticallyAdjustContentInsets={true}
|
||||
bounces={false}
|
||||
contentContainerStyle={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
horizontal={true}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScrollBeginDrag={[Function]}
|
||||
pagingEnabled={true}
|
||||
removeClippedSubviews={true}
|
||||
scrollEnabled={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
@@ -22,14 +22,11 @@ exports[`AnnouncementBanner should match snapshot 1`] = `
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
null,
|
||||
]
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
|
||||
@@ -13,8 +13,6 @@ import {intlShape} from 'react-intl';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import RemoveMarkdown from 'app/components/remove_markdown';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
@@ -25,8 +23,8 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -37,32 +35,40 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
bannerHeight: new Animated.Value(0),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
componentWillMount() {
|
||||
const {bannerDismissed, bannerEnabled, bannerText} = this.props;
|
||||
const showBanner = bannerEnabled && !bannerDismissed && Boolean(bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.bannerText !== prevProps.bannerText ||
|
||||
this.props.bannerEnabled !== prevProps.bannerEnabled ||
|
||||
this.props.bannerDismissed !== prevProps.bannerDismissed
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.bannerText !== nextProps.bannerText ||
|
||||
this.props.bannerEnabled !== nextProps.bannerEnabled ||
|
||||
this.props.bannerDismissed !== nextProps.bannerDismissed
|
||||
) {
|
||||
const showBanner = this.props.bannerEnabled && !this.props.bannerDismissed && Boolean(this.props.bannerText);
|
||||
const showBanner = nextProps.bannerEnabled && !nextProps.bannerDismissed && Boolean(nextProps.bannerText);
|
||||
this.toggleBanner(showBanner);
|
||||
}
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const {intl} = this.context;
|
||||
const {navigator, theme} = this.props;
|
||||
|
||||
const screen = 'ExpandedAnnouncementBanner';
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
navigator.push({
|
||||
screen: 'ExpandedAnnouncementBanner',
|
||||
title: this.context.intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
|
||||
goToScreen(screen, title);
|
||||
};
|
||||
|
||||
toggleBanner = (show = true) => {
|
||||
@@ -83,7 +89,6 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
bannerColor,
|
||||
bannerText,
|
||||
bannerTextColor,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
const bannerStyle = {
|
||||
@@ -101,7 +106,7 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={this.handlePress}
|
||||
style={[style.wrapper, padding(isLandscape)]}
|
||||
style={style.wrapper}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
|
||||
@@ -17,8 +17,8 @@ describe('AnnouncementBanner', () => {
|
||||
bannerEnabled: true,
|
||||
bannerText: 'Banner Text',
|
||||
bannerTextColor: '#fff',
|
||||
navigator: {},
|
||||
theme: Preferences.THEMES.default,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
|
||||
@@ -6,8 +6,6 @@ import {connect} from 'react-redux';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
@@ -22,7 +20,6 @@ function mapStateToProps(state) {
|
||||
bannerText: config.BannerText,
|
||||
bannerTextColor: config.BannerTextColor || '#000',
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AtMention should match snapshot, no highlight 1`] = `
|
||||
<Text>
|
||||
@John.Smith
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<Text
|
||||
style={null}
|
||||
>
|
||||
@John.Smith
|
||||
</Text>
|
||||
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`AtMention should match snapshot, without highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#ff0000",
|
||||
}
|
||||
}
|
||||
>
|
||||
@Victor.Welch
|
||||
</Text>
|
||||
|
||||
</Text>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Clipboard, Text} from 'react-native';
|
||||
import {Clipboard, Platform, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -11,14 +11,13 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isSearchResult: PropTypes.bool,
|
||||
mentionKeys: PropTypes.array.isRequired,
|
||||
mentionName: PropTypes.string.isRequired,
|
||||
mentionStyle: CustomPropTypes.Style,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
@@ -49,14 +48,29 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
goToUserProfile = () => {
|
||||
const {navigator, theme} = this.props;
|
||||
const {intl} = this.context;
|
||||
const screen = 'UserProfile';
|
||||
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
|
||||
const passProps = {
|
||||
userId: this.state.user.id,
|
||||
const options = {
|
||||
screen: 'UserProfile',
|
||||
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
passProps: {
|
||||
userId: this.state.user.id,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
goToScreen(screen, title, passProps);
|
||||
if (Platform.OS === 'ios') {
|
||||
navigator.push(options);
|
||||
} else {
|
||||
navigator.showModal(options);
|
||||
}
|
||||
};
|
||||
|
||||
getUserDetailsFromMentionName(props) {
|
||||
@@ -112,7 +126,7 @@ export default class AtMention extends React.PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys} = this.props;
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle} = this.props;
|
||||
const {user} = this.state;
|
||||
|
||||
if (!user.username) {
|
||||
@@ -120,7 +134,6 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
const suffix = this.props.mentionName.substring(user.username.length);
|
||||
const highlighted = mentionKeys.some((item) => item.key === user.username);
|
||||
|
||||
return (
|
||||
<Text
|
||||
@@ -128,7 +141,7 @@ export default class AtMention extends React.PureComponent {
|
||||
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
|
||||
onLongPress={this.handleLongPress}
|
||||
>
|
||||
<Text style={highlighted ? null : mentionStyle}>
|
||||
<Text style={mentionStyle}>
|
||||
{'@' + displayUsername(user, teammateNameDisplay)}
|
||||
</Text>
|
||||
{suffix}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import AtMention from './at_mention.js';
|
||||
|
||||
describe('AtMention', () => {
|
||||
const baseProps = {
|
||||
usersByUsername: {},
|
||||
mentionKeys: [{key: 'John.Smith'}, {key: 'Jane.Doe'}],
|
||||
teammateNameDisplay: '',
|
||||
mentionName: 'John.Smith',
|
||||
mentionStyle: {color: '#ff0000'},
|
||||
theme: {},
|
||||
};
|
||||
|
||||
test('should match snapshot, no highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, with highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
);
|
||||
|
||||
wrapper.setState({user: {username: 'John.Smith'}});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, without highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
);
|
||||
|
||||
wrapper.setState({user: {username: 'Victor.Welch'}});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersByUsername, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
@@ -13,7 +13,6 @@ function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
usersByUsername: getUsersByUsername(state),
|
||||
mentionKeys: getCurrentUserMentionKeys(state),
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,23 +8,20 @@ import {
|
||||
NativeModules,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import {DocumentPicker} from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
@@ -42,6 +39,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
@@ -67,93 +65,12 @@ export default class AttachmentButton extends PureComponent {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getPermissionDeniedMessage = (source, mediaType = '') => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const applicationName = DeviceInfo.getApplicationName();
|
||||
switch (source) {
|
||||
case 'camera': {
|
||||
if (mediaType === 'video') {
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.camera_video_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your camera',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_video_permission_denied_description',
|
||||
defaultMessage: 'Take videos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your camera',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.camera_photo_permission_denied_description',
|
||||
defaultMessage: 'Take photos and upload them to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your camera.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'storage':
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.storage_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your files',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.storage_permission_denied_description',
|
||||
defaultMessage: 'Upload files to your Mattermost instance. Open Settings to grant Mattermost Read and Write access to files on this device.',
|
||||
}),
|
||||
};
|
||||
case 'video':
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.videos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your videos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.videos_permission_denied_description',
|
||||
defaultMessage: 'Upload videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your video library.',
|
||||
}),
|
||||
};
|
||||
case 'photo':
|
||||
default: {
|
||||
if (Platform.OS === 'android') {
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_title',
|
||||
defaultMessage: '{applicationName} would like to access your photos',
|
||||
}, {applicationName}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.ios.photos_permission_denied_description',
|
||||
defaultMessage: 'Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachPhotoFromCamera = () => {
|
||||
return this.attachFileFromCamera('camera', 'photo');
|
||||
return this.attachFileFromCamera('photo', 'camera');
|
||||
};
|
||||
|
||||
attachFileFromCamera = async (source, mediaType) => {
|
||||
attachFileFromCamera = async (mediaType, source) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {title, text} = this.getPermissionDeniedMessage('camera', mediaType);
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
@@ -164,19 +81,25 @@ export default class AttachmentButton extends PureComponent {
|
||||
waitUntilSaved: true,
|
||||
},
|
||||
permissionDenied: {
|
||||
title,
|
||||
text,
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.camera_permission_denied_title',
|
||||
defaultMessage: 'Camera access is required',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.camera_permission_denied_description',
|
||||
defaultMessage: 'To take photos and videos with your camera, please change your permission settings.',
|
||||
}),
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.android.permission_denied_retry',
|
||||
defaultMessage: 'Set Permission',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}),
|
||||
},
|
||||
};
|
||||
|
||||
const hasCameraPermission = await this.hasPhotoPermission(source, mediaType);
|
||||
const hasPhotoPermission = await this.hasPhotoPermission(source);
|
||||
|
||||
if (hasCameraPermission) {
|
||||
if (hasPhotoPermission) {
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
@@ -187,20 +110,25 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
attachFileFromLibrary = async () => {
|
||||
attachFileFromLibrary = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {title, text} = this.getPermissionDeniedMessage('photo');
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
title,
|
||||
text,
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: 'Photo library access is required',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'To upload images from your library, please change your permission settings.',
|
||||
}),
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.android.permission_denied_retry',
|
||||
defaultMessage: 'Set Permission',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -208,38 +136,39 @@ export default class AttachmentButton extends PureComponent {
|
||||
options.mediaType = 'mixed';
|
||||
}
|
||||
|
||||
const hasPhotoPermission = await this.hasPhotoPermission('photo');
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPhotoPermission) {
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromCamera = () => {
|
||||
return this.attachFileFromCamera('camera', 'video');
|
||||
return this.attachFileFromCamera('video', 'camera');
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {title, text} = this.getPermissionDeniedMessage('video');
|
||||
const options = {
|
||||
videoQuality: 'high',
|
||||
mediaType: 'video',
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
title,
|
||||
text,
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
title: formatMessage({
|
||||
id: 'mobile.android.videos_permission_denied_title',
|
||||
defaultMessage: 'Video library access is required',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.permission_denied_dismiss', defaultMessage: 'Don\'t Allow'}),
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.videos_permission_denied_description',
|
||||
defaultMessage: 'To upload videos from your library, please change your permission settings.',
|
||||
}),
|
||||
reTryTitle: formatMessage({
|
||||
id: 'mobile.android.permission_denied_retry',
|
||||
defaultMessage: 'Set Permission',
|
||||
}),
|
||||
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -257,8 +186,13 @@ export default class AttachmentButton extends PureComponent {
|
||||
const hasPermission = await this.hasStoragePermission();
|
||||
|
||||
if (hasPermission) {
|
||||
try {
|
||||
const res = await DocumentPicker.pick({type: [browseFileTypes]});
|
||||
DocumentPicker.show({
|
||||
filetype: [browseFileTypes],
|
||||
}, async (error, res) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||
const newUri = await ShareExtension.getFilePath(res.uri);
|
||||
@@ -273,22 +207,19 @@ export default class AttachmentButton extends PureComponent {
|
||||
res.uri = decodeURIComponent(res.uri);
|
||||
|
||||
this.uploadFiles([res]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hasPhotoPermission = async (source, mediaType = '') => {
|
||||
hasPhotoPermission = async (source) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = source || 'photo';
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
const hasPermissionToStorage = await Permissions.check(source || 'photo');
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
permissionRequest = await Permissions.request('photo');
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
@@ -299,24 +230,28 @@ export default class AttachmentButton extends PureComponent {
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
id: 'mobile.android.permission_denied_retry',
|
||||
defaultMessage: 'Set permission',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_title',
|
||||
defaultMessage: 'Photo library access is required',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.android.photos_permission_denied_description',
|
||||
defaultMessage: 'To upload images from your library, please change your permission settings.',
|
||||
}),
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
id: 'mobile.android.permission_denied_dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -343,25 +278,35 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage('storage');
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.permission_denied_retry',
|
||||
defaultMessage: 'Set permission',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
formatMessage({
|
||||
id: 'mobile.android.storage_permission_denied_title',
|
||||
defaultMessage: 'File Storage access is required',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.android.storage_permission_denied_description',
|
||||
defaultMessage: 'To upload images from your Android device, please change your permission settings.',
|
||||
}),
|
||||
[
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
id: 'mobile.android.permission_denied_dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
},
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => AndroidOpenSettings.appDetailsSettings(),
|
||||
},
|
||||
grantOption,
|
||||
]
|
||||
);
|
||||
return false;
|
||||
@@ -395,6 +340,21 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'none',
|
||||
});
|
||||
|
||||
// Have to wait to launch the library attachment action.
|
||||
// If we call the action after dismissModal with no delay then the
|
||||
// Wix navigator will dismiss the library attachment modal as well.
|
||||
setTimeout(() => {
|
||||
if (typeof action === 'function') {
|
||||
action();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
showFileAttachmentOptions = () => {
|
||||
const {
|
||||
canBrowseFiles,
|
||||
@@ -418,7 +378,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (canTakePhoto) {
|
||||
items.push({
|
||||
action: this.attachPhotoFromCamera,
|
||||
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera_photo'),
|
||||
defaultMessage: 'Take Photo',
|
||||
@@ -429,7 +389,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (canTakeVideo) {
|
||||
items.push({
|
||||
action: this.attachVideoFromCamera,
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera_video'),
|
||||
defaultMessage: 'Take Video',
|
||||
@@ -440,7 +400,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (canBrowsePhotoLibrary) {
|
||||
items.push({
|
||||
action: this.attachFileFromLibrary,
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
id: t('mobile.file_upload.library'),
|
||||
defaultMessage: 'Photo Library',
|
||||
@@ -451,7 +411,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (canBrowseVideoLibrary && Platform.OS === 'android') {
|
||||
items.push({
|
||||
action: this.attachVideoFromLibraryAndroid,
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
|
||||
text: {
|
||||
id: t('mobile.file_upload.video'),
|
||||
defaultMessage: 'Video Library',
|
||||
@@ -462,7 +422,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (canBrowseFiles) {
|
||||
items.push({
|
||||
action: this.attachFileFromFiles,
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
|
||||
text: {
|
||||
id: t('mobile.file_upload.browse'),
|
||||
defaultMessage: 'Browse Files',
|
||||
@@ -479,7 +439,21 @@ export default class AttachmentButton extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
showModalOverCurrentContext('OptionsModal', {items});
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
items,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -487,20 +461,18 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (wrapper) {
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
type={'opacity'}
|
||||
>
|
||||
{children}
|
||||
</TouchableWithFeedback>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
style={style.buttonContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Icon
|
||||
size={30}
|
||||
@@ -508,7 +480,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -529,4 +501,4 @@ const style = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
});
|
||||