forked from Ivasoft/mattermost-mobile
Compare commits
55 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d520d1909 | ||
|
|
d15420ae54 | ||
|
|
08fe8e529b | ||
|
|
44669d93bb | ||
|
|
cfc65c0250 | ||
|
|
9eb1aa1ad0 | ||
|
|
5d923f3c4a | ||
|
|
01eb3ce8b6 | ||
|
|
0fa96be8ee | ||
|
|
c983caf583 | ||
|
|
95f321c518 | ||
|
|
5ec12c8784 | ||
|
|
993143b0f8 | ||
|
|
53c4df74c6 | ||
|
|
fdc894c00f | ||
|
|
04bedbc954 | ||
|
|
9dd36bf15e | ||
|
|
9d28eb043c | ||
|
|
188bfecf17 | ||
|
|
3eb8d3857b | ||
|
|
e9e1dc0541 | ||
|
|
c55dcaf598 | ||
|
|
d5dd4380d9 | ||
|
|
486917d692 | ||
|
|
53657536fc | ||
|
|
cd12480577 | ||
|
|
643d45b33c | ||
|
|
d3b5281ecb | ||
|
|
d5ea75171c | ||
|
|
73d20fdcdf | ||
|
|
2eb723a6dc | ||
|
|
5fafe376fa | ||
|
|
0818489b47 | ||
|
|
14593339b3 | ||
|
|
80fad8c11c | ||
|
|
f2e06aa304 | ||
|
|
41bcc75df9 | ||
|
|
03692f1975 | ||
|
|
8927e5921a | ||
|
|
bd4a119c05 | ||
|
|
6b4b4ce75f | ||
|
|
cdc020fc9c | ||
|
|
5f2d840f27 | ||
|
|
dca0d5e75b | ||
|
|
4dbdf42ebd | ||
|
|
cf2262dbc1 | ||
|
|
478bf42b62 | ||
|
|
aada9efb2b | ||
|
|
4ada33b50d | ||
|
|
b8540b42dd | ||
|
|
9bbbf67cc8 | ||
|
|
54f403c354 | ||
|
|
80282c6df1 | ||
|
|
f99d260628 | ||
|
|
2bd67deeea |
@@ -1,209 +1,5 @@
|
||||
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:
|
||||
@@ -211,290 +7,17 @@ jobs:
|
||||
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
|
||||
- checkout
|
||||
- run: |
|
||||
echo assets/base/config.json
|
||||
cat assets/base/config.json
|
||||
# Avoid installing pods
|
||||
touch .podinstall
|
||||
# Run tests
|
||||
make test || exit 1
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
pr-test:
|
||||
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
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.92.0
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -22,6 +22,7 @@ build/
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
xcshareddata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
@@ -30,8 +31,7 @@ DerivedData
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
.podinstall
|
||||
xcshareddata/
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
@@ -39,10 +39,6 @@ ios/Pods
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
android/app/bin
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# node.js
|
||||
#
|
||||
@@ -88,6 +84,10 @@ ios/sentry.properties
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
# Pods
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,101 +1,5 @@
|
||||
# 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
|
||||
|
||||
32
Makefile
32
Makefile
@@ -64,18 +64,24 @@ 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 RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
|
||||
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
|
||||
|
||||
@# Need to copy custom RNCNetInfo.m that checks for internet connectivity instead of reaching a host by default
|
||||
@cp ./native_modules/RNCNetInfo.m node_modules/@react-native-community/netinfo/ios/RNCNetInfo.m
|
||||
|
||||
@rm -f node_modules/intl/.babelrc
|
||||
@# Hack to get react-intl and its dependencies to work with react-native
|
||||
@@ -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
|
||||
@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
|
||||
|
||||
@./node_modules/.bin/patch-package
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
$(call start_packager)
|
||||
@@ -185,7 +197,14 @@ 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 .
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.ipa os_type:iOS
|
||||
@rm -rf build-ios/
|
||||
$(call stop_packager)
|
||||
|
||||
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
|
||||
@@ -201,7 +220,12 @@ ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version o
|
||||
$(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
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.apk os_type:Android
|
||||
$(call stop_packager)
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
|
||||
231
NOTICE.txt
231
NOTICE.txt
@@ -78,41 +78,6 @@ 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.
|
||||
@@ -148,41 +113,6 @@ 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.
|
||||
@@ -514,39 +444,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.
|
||||
@@ -1681,41 +1578,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.
|
||||
@@ -2205,6 +2067,41 @@ 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.
|
||||
@@ -2480,29 +2377,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 +2482,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.
|
||||
|
||||
@@ -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).]
|
||||
|
||||
@@ -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: "metro.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,28 +105,6 @@ 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
|
||||
|
||||
@@ -135,13 +113,18 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libjsc.so'
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative60"
|
||||
versionCode 258
|
||||
versionName "1.26.2"
|
||||
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
|
||||
versionCode 239
|
||||
versionName "1.24.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
@@ -179,7 +162,7 @@ android {
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
signingConfig null
|
||||
matchingFallbacks = ['release']
|
||||
matchingFallbacks = ['debug', 'release']
|
||||
}
|
||||
}
|
||||
// applicationVariants are e.g. debug, release
|
||||
@@ -228,21 +211,15 @@ 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
|
||||
}
|
||||
// Make sure to put android-jsc at the top
|
||||
implementation "org.webkit:android-jsc-intl:r241213"
|
||||
|
||||
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')
|
||||
@@ -258,23 +235,22 @@ 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-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
|
||||
|
||||
7
android/app/proguard-rules.pro
vendored
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 *;
|
||||
#}
|
||||
|
||||
@@ -29,8 +29,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" />
|
||||
@@ -53,8 +52,7 @@
|
||||
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" />
|
||||
|
||||
@@ -20,27 +20,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 +44,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 +53,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 +74,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,62 +87,41 @@ 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");
|
||||
final String ackId = data.getString("ack_id");
|
||||
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(ackId, type);
|
||||
}
|
||||
|
||||
// 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("sender_name"), data.getString("channel_name"), data.getString("message"));
|
||||
data.putLong("time", new Date().getTime());
|
||||
data.putString("sender_name", senderName);
|
||||
data.putString("sender_id", data.getString("sender_id"));
|
||||
@@ -161,13 +131,10 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
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,141 +149,59 @@ 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 version = bundle.getString("version");
|
||||
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 channelName = bundle.getString("channel_name");
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String badge = bundle.getString("badge");
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
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");
|
||||
if (version != null && version.equals("v2")) {
|
||||
title = bundle.getString("channel_name");
|
||||
title = channelName;
|
||||
} else {
|
||||
title = bundle.getString("title");
|
||||
}
|
||||
@@ -326,103 +211,149 @@ 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);
|
||||
Bundle b = bundle.getBundle("userInfo");
|
||||
if (b == null) {
|
||||
b = new Bundle();
|
||||
}
|
||||
b.putString("channel_id", channelId);
|
||||
notification.addExtras(b);
|
||||
|
||||
if (conversationTitle != null && (!conversationTitle.startsWith("@") || channelName != senderName)) {
|
||||
messagingStyle.setConversationTitle(conversationTitle);
|
||||
}
|
||||
}
|
||||
int smallIconResId;
|
||||
int largeIconResId;
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(senderName)) {
|
||||
senderName = getSenderName(senderName, channelName, bundle.getString("message"));
|
||||
}
|
||||
|
||||
String personId = senderId;
|
||||
if (!android.text.TextUtils.isEmpty(channelName)) {
|
||||
personId = channelId;
|
||||
}
|
||||
|
||||
Notification.MessagingStyle messagingStyle;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("")
|
||||
.build();
|
||||
messagingStyle = new Notification.MessagingStyle(sender);
|
||||
}
|
||||
|
||||
if (title != null && (!title.startsWith("@") || channelName != senderName)) {
|
||||
messagingStyle
|
||||
.setConversationTitle(title);
|
||||
}
|
||||
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
list.add(bundle);
|
||||
}
|
||||
|
||||
int listCount = list.size() - 1;
|
||||
for (int i = listCount; i >= 0; i--) {
|
||||
Bundle data = list.get(i);
|
||||
String message = data.getString("message");
|
||||
String senderId = data.getString("sender_id");
|
||||
if (senderId == null) {
|
||||
senderId = "sender_id";
|
||||
}
|
||||
Bundle userInfoBundle = data.getBundle("userInfo");
|
||||
String senderName = getSenderName(data);
|
||||
if (userInfoBundle != null) {
|
||||
boolean localPushNotificationTest = userInfoBundle.getBoolean("localTest");
|
||||
if (localPushNotificationTest) {
|
||||
senderName = "Test";
|
||||
}
|
||||
String previousPersonName = getSenderName(data.getString("sender_name"), channelName, message);
|
||||
String previousPersonId = data.getString("sender_id");
|
||||
|
||||
if (title == null || !android.text.TextUtils.isEmpty(previousPersonName)) {
|
||||
message = removeSenderFromMessage(previousPersonName, channelName, message);
|
||||
}
|
||||
|
||||
if (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);
|
||||
messagingStyle.addMessage(message, data.getLong("time"), previousPersonName);
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName(senderName)
|
||||
.build();
|
||||
messagingStyle.addMessage(message, timestamp, sender);
|
||||
.setKey(previousPersonId)
|
||||
.setName(previousPersonName)
|
||||
.build();
|
||||
messagingStyle.addMessage(message, data.getLong("time"), 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;
|
||||
}
|
||||
notification
|
||||
.setContentIntent(intent)
|
||||
.setGroupSummary(true)
|
||||
.setStyle(messagingStyle)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
|
||||
NotificationChannel notificationChannel = mHighImportanceChannel;
|
||||
|
||||
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 +363,65 @@ 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");
|
||||
if (channelName != null && channelName.startsWith("@")) {
|
||||
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 senderName, String channelName, String message) {
|
||||
if (senderName != null) {
|
||||
return senderName;
|
||||
} else 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 name = message.split(":")[0];
|
||||
if (name != message) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return getConversationTitle(bundle);
|
||||
return " ";
|
||||
}
|
||||
|
||||
private String removeSenderNameFromMessage(String message, String senderName) {
|
||||
return message.replaceFirst(senderName, "").replaceFirst(": ", "").trim();
|
||||
private String removeSenderFromMessage(String senderName, String channelName, String message) {
|
||||
String sender = String.format("%s", getSenderName(senderName, channelName, message));
|
||||
return message.replaceFirst(sender, "").replaceFirst(": ", "").trim();
|
||||
}
|
||||
|
||||
private void notificationReceiptDelivery(String ackId, String 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 void notificationReceiptDelivery(String ackId, String type) {
|
||||
ReceiptDelivery.send(context, ackId, type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.reactnativenavigation.NavigationActivity;
|
||||
|
||||
@@ -10,5 +10,19 @@ public class MainActivity extends NavigationActivity {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.os.Bundle;
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -21,16 +22,15 @@ import com.wix.reactnativenotifications.RNNotificationsModule;
|
||||
import io.tradle.react.LocalAuthModule;
|
||||
import com.gantix.JailMonkey.JailMonkeyModule;
|
||||
import com.RNFetchBlob.RNFetchBlob;
|
||||
import io.sentry.RNSentryModule;
|
||||
import io.sentry.RNSentryEventEmitter;
|
||||
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule;
|
||||
import com.inprogress.reactnativeyoutube.YouTubeStandaloneModule;
|
||||
import com.philipphecht.RNDocViewerModule;
|
||||
import io.github.elyx0.reactnativedocumentpicker.DocumentPickerModule;
|
||||
import com.reactlibrary.RNReactNativeDocViewerModule;
|
||||
import com.reactnativedocumentpicker.DocumentPicker;
|
||||
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;
|
||||
|
||||
@@ -124,7 +124,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
case "RNLocalAuth":
|
||||
return new LocalAuthModule(reactContext);
|
||||
case "JailMonkey":
|
||||
return new JailMonkeyModule(reactContext, false);
|
||||
return new JailMonkeyModule(reactContext);
|
||||
case "RNFetchBlob":
|
||||
return new RNFetchBlob(reactContext);
|
||||
case "MattermostManaged":
|
||||
@@ -133,18 +133,20 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return NotificationPreferencesModule.getInstance(instance, reactContext);
|
||||
case "RNTextInputReset":
|
||||
return new RNTextInputResetModule(reactContext);
|
||||
case "RNSentry":
|
||||
return new RNSentryModule(reactContext);
|
||||
case "RNSentryEventEmitter":
|
||||
return new RNSentryEventEmitter(reactContext);
|
||||
case "ReactNativeExceptionHandler":
|
||||
return new ReactNativeExceptionHandlerModule(reactContext);
|
||||
case "YouTubeStandaloneModule":
|
||||
return new YouTubeStandaloneModule(reactContext);
|
||||
case "RNDocViewer":
|
||||
return new RNDocViewerModule(reactContext);
|
||||
case "RNReactNativeDocViewer":
|
||||
return new RNReactNativeDocViewerModule(reactContext);
|
||||
case "RNDocumentPicker":
|
||||
return new DocumentPickerModule(reactContext);
|
||||
return new DocumentPicker(reactContext);
|
||||
case "RNKeychainManager":
|
||||
return new KeychainModule(reactContext);
|
||||
case "RNSentry":
|
||||
return new RNSentryModule(reactContext);
|
||||
case AsyncStorageModule.NAME:
|
||||
return new AsyncStorageModule(reactContext);
|
||||
case NetInfoModule.NAME:
|
||||
@@ -177,12 +179,13 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
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("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
|
||||
map.put("RNSentryEventEmitter", new ReactModuleInfo("RNSentryEventEmitter", "com.sentry.RNSentryEventEmitter", 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("RNReactNativeDocViewer", new ReactModuleInfo("RNReactNativeDocViewer", "com.reactlibrary.RNReactNativeDocViewerModule", false, false, false, false, false));
|
||||
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "com.reactnativedocumentpicker.DocumentPicker", 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));
|
||||
@@ -192,7 +195,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
};
|
||||
}
|
||||
},
|
||||
new FastImageViewPackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new SvgPackage(),
|
||||
new LinearGradientPackage(),
|
||||
@@ -207,6 +209,8 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
|
||||
registerActivityLifecycleCallbacks(new ManagedActivityLifecycleCallbacks());
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
|
||||
RealPathUtil.deleteTempFiles(tempFolder);
|
||||
@@ -266,7 +270,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
}
|
||||
|
||||
public synchronized Bundle getManagedConfig() {
|
||||
if (mManagedConfig != null && mManagedConfig.size() > 0) {
|
||||
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
|
||||
return mManagedConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.content.Context;
|
||||
import android.content.RestrictionsManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.view.WindowManager;
|
||||
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.ReactContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
public class ManagedActivityLifecycleCallbacks implements ActivityLifecycleCallbacks {
|
||||
private static final String TAG = ManagedActivityLifecycleCallbacks.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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
Bundle managedConfig = MainApplication.instance.getManagedConfig();
|
||||
if (managedConfig != null && activity != null) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
ReactContext ctx = MainApplication.instance.getRunningReactContext();
|
||||
Bundle managedConfig = MainApplication.instance.getManagedConfig();
|
||||
|
||||
if (ctx != null) {
|
||||
Bundle newConfig = MainApplication.instance.loadManagedConfig(ctx);
|
||||
if (!equalBundles(newConfig, managedConfig)) {
|
||||
Log.i(TAG, "onResumed Managed Configuration Changed");
|
||||
sendConfigChanged(newConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) {
|
||||
Bundle managedConfig = MainApplication.instance.getManagedConfig();
|
||||
|
||||
if (managedConfig != null) {
|
||||
try {
|
||||
activity.unregisterReceiver(restrictionsReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Just ignore this cause the receiver wasn't registered for this activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,8 @@ 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;
|
||||
@@ -22,34 +15,14 @@ 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,6 +42,15 @@ 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 {
|
||||
@@ -114,110 +96,4 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule implemen
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,9 +21,6 @@ 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;
|
||||
@@ -63,7 +60,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
String token = map.getString("password");
|
||||
String serverUrl = map.getString("service");
|
||||
|
||||
Log.i("ReactNative", String.format("URL=%s", serverUrl));
|
||||
Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token));
|
||||
replyToMessage(serverUrl, token, notificationId, message);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +84,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 +96,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 +104,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 +114,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 +127,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);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import androidx.core.os.BuildCompat;
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
||||
import android.support.v4.os.BuildCompat;
|
||||
import android.text.InputType;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.lang.System;
|
||||
|
||||
@@ -19,14 +18,13 @@ 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) {
|
||||
public static void send (Context context, final String ackId, final String type) {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
|
||||
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
||||
@@ -48,23 +46,18 @@ public class ReceiptDelivery {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token));
|
||||
execute(serverUrl, token, ackId, type);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
if (token == null) {
|
||||
promise.reject("Receipt delivery failure", "Invalid token");
|
||||
protected static void execute(String serverUrl, String token, String ackId, String type) {
|
||||
if (token == null || serverUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverUrl == null) {
|
||||
promise.reject("Receipt delivery failure", "Invalid server URL");
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
long receivedAt = System.currentTimeMillis();
|
||||
|
||||
@@ -74,11 +67,8 @@ public class ReceiptDelivery {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -96,24 +86,9 @@ public class ReceiptDelivery {
|
||||
.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);
|
||||
client.newCall(request).execute();
|
||||
} 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>
|
||||
|
||||
@@ -15,7 +15,7 @@ buildscript {
|
||||
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
|
||||
|
||||
@@ -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
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
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,10 +1,6 @@
|
||||
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'
|
||||
@@ -19,6 +15,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'
|
||||
@@ -38,7 +36,7 @@ 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'
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
getMyChannelMember,
|
||||
getRedirectChannelNameForTeam,
|
||||
getChannelsNameMapInTeam,
|
||||
isManuallyUnread,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
@@ -380,23 +379,45 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
}
|
||||
|
||||
let previousChannelId;
|
||||
if (!fromPushNotification && !sameChannel) {
|
||||
previousChannelId = currentChannelId;
|
||||
}
|
||||
|
||||
const actions = [
|
||||
selectChannel(channelId),
|
||||
getChannelStats(channelId),
|
||||
setChannelDisplayName(channel.display_name),
|
||||
setInitialPostVisibility(channelId),
|
||||
{
|
||||
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
},
|
||||
setChannelLoading(false),
|
||||
setLastChannelForTeam(currentTeamId, channelId),
|
||||
selectChannelWithMember(channelId, channel, member),
|
||||
{
|
||||
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
|
||||
teamId: currentTeamId,
|
||||
channelId,
|
||||
},
|
||||
];
|
||||
|
||||
let markPreviousChannelId;
|
||||
if (!fromPushNotification && !sameChannel) {
|
||||
markPreviousChannelId = currentChannelId;
|
||||
actions.push({
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: currentChannelId,
|
||||
channel: getChannel(state, currentChannelId),
|
||||
member: getMyChannelMember(state, currentChannelId),
|
||||
});
|
||||
}
|
||||
|
||||
if (!fromPushNotification) {
|
||||
actions.push({
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
channel,
|
||||
member,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(markChannelViewedAndRead(channelId, previousChannelId));
|
||||
|
||||
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -448,17 +469,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();
|
||||
@@ -676,27 +686,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,27 +5,27 @@ 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 * as ChannelActions from 'app/actions/views/channel';
|
||||
const {
|
||||
handleSelectChannel,
|
||||
import {
|
||||
handleSelectChannelByName,
|
||||
loadPostsIfNecessaryWithRetry,
|
||||
} = ChannelActions;
|
||||
} from 'app/actions/views/channel';
|
||||
|
||||
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/selectors/entities/channels', () => ({
|
||||
getChannel: () => ({data: 'received-channel-id'}),
|
||||
getCurrentChannelId: () => 'current-channel-id',
|
||||
getMyChannelMember: () => ({data: {member: {}}}),
|
||||
}));
|
||||
|
||||
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'}),
|
||||
markChannelAsRead: jest.fn(),
|
||||
markChannelAsViewed: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -130,11 +130,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);
|
||||
|
||||
@@ -243,38 +238,4 @@ describe('Actions.Views.Channel', () => {
|
||||
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) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
import {getDeviceTimezone} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
@@ -51,8 +51,7 @@ export function handleSuccessfulLogin() {
|
||||
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
if (enableTimezone) {
|
||||
const timezone = await getDeviceTimezoneAsync();
|
||||
dispatch(autoUpdateTimezone(timezone));
|
||||
dispatch(autoUpdateTimezone(getDeviceTimezone()));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
|
||||
@@ -47,7 +47,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 +84,7 @@ export function loadFromPushNotification(notification) {
|
||||
dispatch(selectTeam({id: teamId}));
|
||||
}
|
||||
|
||||
dispatch(handleSelectChannel(channelId, true));
|
||||
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ exports[`profile_picture_button should match snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
|
||||
@@ -37,18 +37,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -16,7 +16,6 @@ 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,
|
||||
onPostPress: PropTypes.func,
|
||||
@@ -112,7 +111,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 +119,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 +126,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ 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';
|
||||
|
||||
@@ -257,8 +257,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,9 +278,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
res.uri = decodeURIComponent(res.uri);
|
||||
|
||||
this.uploadFiles([res]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ import AttachmentButton from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
|
||||
jest.mock('Platform', () => {
|
||||
const Platform = require.requireActual('Platform');
|
||||
Platform.OS = 'ios';
|
||||
return Platform;
|
||||
});
|
||||
|
||||
describe('AttachmentButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
|
||||
@@ -13,7 +13,6 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
import ChannelMention from './channel_mention';
|
||||
@@ -35,8 +34,6 @@ export default class Autocomplete extends PureComponent {
|
||||
valueEvent: PropTypes.string,
|
||||
cursorPositionEvent: PropTypes.string,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
expandDown: PropTypes.bool,
|
||||
onVisible: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -44,7 +41,6 @@ export default class Autocomplete extends PureComponent {
|
||||
cursorPosition: 0,
|
||||
enableDateSuggestion: false,
|
||||
nestedScrollEnabled: false,
|
||||
onVisible: emptyFunction,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
@@ -77,8 +73,6 @@ export default class Autocomplete extends PureComponent {
|
||||
keyboardOffset: 0,
|
||||
value: props.value,
|
||||
};
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -107,11 +101,6 @@ export default class Autocomplete extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const visible = Boolean(this.containerRef.current?._children.length);
|
||||
this.props.onVisible(visible);
|
||||
}
|
||||
|
||||
onChangeText = (value) => {
|
||||
this.props.onChangeText(value, true);
|
||||
};
|
||||
@@ -171,37 +160,32 @@ export default class Autocomplete extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {theme, isSearch, expandDown} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
const wrapperStyles = [];
|
||||
const containerStyles = [];
|
||||
if (isSearch) {
|
||||
wrapperStyles.push(style.base, style.searchContainer);
|
||||
containerStyles.push(style.content);
|
||||
const wrapperStyle = [];
|
||||
const containerStyle = [];
|
||||
if (this.props.isSearch) {
|
||||
wrapperStyle.push(style.base, style.searchContainer);
|
||||
containerStyle.push(style.content);
|
||||
} else {
|
||||
const containerStyle = expandDown ? style.containerExpandDown : style.container;
|
||||
containerStyles.push(style.base, containerStyle);
|
||||
containerStyle.push(style.base, style.container);
|
||||
}
|
||||
|
||||
// We always need to render something, but we only draw the borders when we have results to show
|
||||
const {atMentionCount, channelMentionCount, emojiCount, commandCount, dateCount, cursorPosition, value} = this.state;
|
||||
if (atMentionCount + channelMentionCount + emojiCount + commandCount + dateCount > 0) {
|
||||
if (this.props.isSearch) {
|
||||
wrapperStyles.push(style.bordersSearch);
|
||||
wrapperStyle.push(style.bordersSearch);
|
||||
} else {
|
||||
containerStyles.push(style.borders);
|
||||
containerStyle.push(style.borders);
|
||||
}
|
||||
}
|
||||
|
||||
const maxListHeight = this.maxListHeight();
|
||||
|
||||
return (
|
||||
<View style={wrapperStyles}>
|
||||
<View
|
||||
ref={this.containerRef}
|
||||
style={containerStyles}
|
||||
>
|
||||
<View style={wrapperStyle}>
|
||||
<View style={containerStyle}>
|
||||
<AtMention
|
||||
{...this.props}
|
||||
cursorPosition={cursorPosition}
|
||||
@@ -272,9 +256,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
bottom: 0,
|
||||
},
|
||||
containerExpandDown: {
|
||||
top: 0,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -34,12 +34,7 @@ export default class AutocompleteSectionHeader extends PureComponent {
|
||||
defaultMessage={defaultMessage}
|
||||
style={style.sectionText}
|
||||
/>
|
||||
{loading &&
|
||||
<ActivityIndicator
|
||||
color={theme.centerChannelColor}
|
||||
size='small'
|
||||
/>
|
||||
}
|
||||
{loading && <ActivityIndicator size='small'/>}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
let isBot = false;
|
||||
let isGuest = false;
|
||||
if (channel?.type === General.DM_CHANNEL) {
|
||||
if (channel.type === General.DM_CHANNEL) {
|
||||
const teammate = getUser(state, channel.teammate_id);
|
||||
if (teammate) {
|
||||
displayName = teammate.username;
|
||||
@@ -31,8 +31,8 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
return {
|
||||
displayName,
|
||||
name: channel?.name,
|
||||
type: channel?.type,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
isBot,
|
||||
isGuest,
|
||||
theme: getTheme(state),
|
||||
|
||||
@@ -15,8 +15,6 @@ import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import Emoji from 'app/components/emoji';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {BuiltInEmojis} from 'app/utils/emojis';
|
||||
import {getEmojiByName} from 'app/utils/emoji_utils';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
|
||||
@@ -140,16 +138,10 @@ export default class EmojiSuggestion extends Component {
|
||||
// We are going to set a double : on iOS to prevent the auto correct from taking over and replacing it
|
||||
// with the wrong value, this is a hack but I could not found another way to solve it
|
||||
let completedDraft;
|
||||
let prefix = ':';
|
||||
if (Platform.OS === 'ios') {
|
||||
prefix = '::';
|
||||
}
|
||||
|
||||
const emojiData = getEmojiByName(emoji);
|
||||
if (emojiData?.filename && !BuiltInEmojis.includes(emojiData.filename)) {
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, String.fromCodePoint(parseInt(emojiData.filename, 16)));
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `::${emoji}: `);
|
||||
} else {
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `${prefix}${emoji}: `);
|
||||
completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
|
||||
}
|
||||
|
||||
if (value.length > cursorPosition) {
|
||||
@@ -158,7 +150,7 @@ export default class EmojiSuggestion extends Component {
|
||||
|
||||
onChangeText(completedDraft);
|
||||
|
||||
if (Platform.OS === 'ios' && (!emojiData?.filename || BuiltInEmojis.includes(emojiData?.filename))) {
|
||||
if (Platform.OS === 'ios') {
|
||||
// This is the second part of the hack were we replace the double : with just one
|
||||
// after the auto correct vanished
|
||||
setTimeout(() => {
|
||||
@@ -187,7 +179,6 @@ export default class EmojiSuggestion extends Component {
|
||||
<View style={style.emoji}>
|
||||
<Emoji
|
||||
emojiName={item}
|
||||
textStyle={style.emojiText}
|
||||
size={20}
|
||||
/>
|
||||
</View>
|
||||
@@ -235,10 +226,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
fontSize: 13,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
emojiText: {
|
||||
color: '#000',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
|
||||
@@ -36,7 +36,6 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
errorText: PropTypes.node,
|
||||
roundedBorders: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -120,7 +119,6 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
showRequiredAsterisk,
|
||||
roundedBorders,
|
||||
isLandscape,
|
||||
disabled,
|
||||
} = this.props;
|
||||
const {selectedText} = this.state;
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -190,10 +188,9 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
{labelContent}
|
||||
</View>
|
||||
<TouchableWithFeedback
|
||||
style={disabled ? style.disabled : null}
|
||||
style={style.flex}
|
||||
onPress={this.goToSelectorScreen}
|
||||
type={'opacity'}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View style={inputStyle}>
|
||||
<Text
|
||||
@@ -287,8 +284,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.errorTextColor,
|
||||
fontSize: 14,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -35,7 +35,9 @@ export default class Badge extends PureComponent {
|
||||
|
||||
this.mounted = false;
|
||||
this.layoutReady = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.panResponder = PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
onMoveShouldSetPanResponder: () => true,
|
||||
@@ -59,19 +61,19 @@ export default class Badge extends PureComponent {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
setBadgeRef = (ref) => {
|
||||
this.badgeRef = ref;
|
||||
};
|
||||
|
||||
handlePress = () => {
|
||||
if (this.props.onPress) {
|
||||
this.props.onPress();
|
||||
}
|
||||
};
|
||||
|
||||
setBadgeRef = (ref) => {
|
||||
this.badgeContainerRef = ref;
|
||||
};
|
||||
|
||||
setNativeProps = (props) => {
|
||||
if (this.mounted && this.badgeRef) {
|
||||
this.badgeRef.setNativeProps(props);
|
||||
if (this.mounted && this.badgeContainerRef) {
|
||||
this.badgeContainerRef.setNativeProps(props);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import CustomListRow from 'app/components/custom_list/custom_list_row';
|
||||
export default class ChannelListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
isArchived: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
channel: PropTypes.object.isRequired,
|
||||
...CustomListRow.propTypes,
|
||||
@@ -54,7 +53,7 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
<View style={style.container}>
|
||||
<View style={style.titleContainer}>
|
||||
<Icon
|
||||
name={this.props.isArchived ? 'archive' : 'globe'}
|
||||
name='globe'
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text style={style.displayName}>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, Keyboard, Platform, RefreshControl, SectionList, Text, View} from 'react-native';
|
||||
import {FlatList, Keyboard, Platform, SectionList, Text, View} from 'react-native';
|
||||
|
||||
import {ListTypes} from 'app/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
@@ -18,7 +18,6 @@ export default class CustomList extends PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
extraData: PropTypes.any,
|
||||
canRefresh: PropTypes.bool,
|
||||
listType: PropTypes.oneOf([FLATLIST, SECTIONLIST]),
|
||||
loading: PropTypes.bool,
|
||||
loadingComponent: PropTypes.element,
|
||||
@@ -36,11 +35,10 @@ export default class CustomList extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
canRefresh: true,
|
||||
isLandscape: false,
|
||||
listType: FLATLIST,
|
||||
showNoResults: true,
|
||||
shouldRenderSeparator: true,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -112,20 +110,9 @@ export default class CustomList extends PureComponent {
|
||||
};
|
||||
|
||||
renderFlatList = () => {
|
||||
const {canRefresh, data, extraData, theme, onRefresh, refreshing} = this.props;
|
||||
const {data, extraData, theme, onRefresh, refreshing} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
let refreshControl;
|
||||
if (canRefresh) {
|
||||
refreshControl = (
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={[theme.centerChannelColor]}
|
||||
tintColor={theme.centerChannelColor}
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={style.container}
|
||||
@@ -141,7 +128,8 @@ export default class CustomList extends PureComponent {
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onLayout={this.handleLayout}
|
||||
onScroll={this.handleScroll}
|
||||
refreshControl={refreshControl}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={refreshing}
|
||||
ref={this.setListRef}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
|
||||
@@ -11,7 +11,6 @@ import CustomList, {FLATLIST, SECTIONLIST} from './index';
|
||||
|
||||
describe('CustomList', () => {
|
||||
const baseProps = {
|
||||
canRefresh: false,
|
||||
data: [{username: 'username_1'}, {username: 'username_2'}],
|
||||
listType: FLATLIST,
|
||||
loading: false,
|
||||
|
||||
@@ -74,7 +74,6 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -106,7 +105,6 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -204,7 +202,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -236,7 +233,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -336,7 +332,6 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -368,7 +363,6 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -479,7 +473,6 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -511,7 +504,6 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
|
||||
@@ -11,8 +11,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
extraScrollHeight={0}
|
||||
keyboardOpeningTime={250}
|
||||
keyboardShouldPersistTaps="always"
|
||||
onKeyboardDidHide={[Function]}
|
||||
onKeyboardDidShow={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
@@ -75,7 +73,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance="light"
|
||||
maxLength={64}
|
||||
onChangeText={[Function]}
|
||||
placeholder={
|
||||
Object {
|
||||
@@ -201,7 +198,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
@@ -237,6 +233,13 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<ForwardRef(forwardConnectRef)
|
||||
cursorPosition={6}
|
||||
maxHeight={200}
|
||||
nestedScrollEnabled={true}
|
||||
onChangeText={[Function]}
|
||||
value="header"
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
@@ -284,21 +287,7 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
value="header"
|
||||
/>
|
||||
</View>
|
||||
<ForwardRef(forwardConnectRef)
|
||||
cursorPosition={6}
|
||||
expandDown={true}
|
||||
maxHeight={200}
|
||||
nestedScrollEnabled={true}
|
||||
onChangeText={[Function]}
|
||||
value="header"
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"zIndex": -1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<FormattedText
|
||||
defaultMessage="Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com)."
|
||||
id="channel_modal.headerHelp"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Platform,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
findNodeHandle,
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
@@ -66,11 +67,8 @@ export default class EditChannelInfo extends PureComponent {
|
||||
this.urlInput = React.createRef();
|
||||
this.purposeInput = React.createRef();
|
||||
this.headerInput = React.createRef();
|
||||
this.lastText = React.createRef();
|
||||
this.scroll = React.createRef();
|
||||
|
||||
this.state = {
|
||||
keyboardVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
blur = () => {
|
||||
@@ -156,37 +154,12 @@ export default class EditChannelInfo extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
onHeaderLayout = ({nativeEvent}) => {
|
||||
this.setState({headerPosition: nativeEvent.layout.y});
|
||||
}
|
||||
|
||||
onKeyboardDidShow = () => {
|
||||
this.setState({keyboardVisible: true});
|
||||
|
||||
if (this.state.headerHasFocus) {
|
||||
this.setState({headerHasFocus: false});
|
||||
this.scrollHeaderToTop();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyboardDidHide = () => {
|
||||
this.setState({keyboardVisible: false});
|
||||
}
|
||||
|
||||
onHeaderFocus = () => {
|
||||
if (this.state.keyboardVisible) {
|
||||
this.scrollHeaderToTop();
|
||||
} else {
|
||||
this.setState({headerHasFocus: true});
|
||||
scrollToEnd = () => {
|
||||
if (this.scroll?.current && this.lastText?.current) {
|
||||
this.scroll.current.scrollToFocusedInput(findNodeHandle(this.lastText.current));
|
||||
}
|
||||
};
|
||||
|
||||
scrollHeaderToTop = () => {
|
||||
if (this.scroll.current) {
|
||||
this.scroll.current.scrollToPosition(0, this.state.headerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
@@ -197,10 +170,8 @@ export default class EditChannelInfo extends PureComponent {
|
||||
header,
|
||||
purpose,
|
||||
isLandscape,
|
||||
error,
|
||||
saving,
|
||||
} = this.props;
|
||||
const {keyboardVisible} = this.state;
|
||||
const {error, saving} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -211,7 +182,7 @@ export default class EditChannelInfo extends PureComponent {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading color={theme.centerChannelColor}/>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -234,9 +205,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
ref={this.scroll}
|
||||
style={style.container}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
onKeyboardDidShow={this.onKeyboardDidShow}
|
||||
onKeyboardDidHide={this.onKeyboardDidHide}
|
||||
enableAutomaticScroll={!keyboardVisible}
|
||||
>
|
||||
{displayError}
|
||||
<TouchableWithoutFeedback onPress={this.blur}>
|
||||
@@ -262,7 +230,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
maxLength={64}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
@@ -309,10 +276,7 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
onLayout={this.onHeaderLayout}
|
||||
style={[style.titleContainer15, padding(isLandscape)]}
|
||||
>
|
||||
<View style={[style.titleContainer15, padding(isLandscape)]}>
|
||||
<FormattedText
|
||||
style={style.title}
|
||||
id='channel_modal.header'
|
||||
@@ -324,6 +288,13 @@ export default class EditChannelInfo extends PureComponent {
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</View>
|
||||
<Autocomplete
|
||||
cursorPosition={header.length}
|
||||
maxHeight={200}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
/>
|
||||
<View style={[style.inputContainer, padding(isLandscape)]}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
ref={this.headerInput}
|
||||
@@ -336,22 +307,14 @@ export default class EditChannelInfo extends PureComponent {
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
onFocus={this.onHeaderFocus}
|
||||
onFocus={this.scrollToEnd}
|
||||
textAlignVertical='top'
|
||||
underlineColorAndroid='transparent'
|
||||
disableFullscreenUI={true}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
/>
|
||||
</View>
|
||||
<Autocomplete
|
||||
cursorPosition={header.length}
|
||||
maxHeight={200}
|
||||
onChangeText={this.onHeaderChangeText}
|
||||
value={header}
|
||||
nestedScrollEnabled={true}
|
||||
expandDown={true}
|
||||
/>
|
||||
<View style={style.headerHelpText}>
|
||||
<View ref={this.lastText}>
|
||||
<FormattedText
|
||||
style={[style.helpText, padding(isLandscape)]}
|
||||
id='channel_modal.headerHelp'
|
||||
@@ -418,8 +381,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
headerHelpText: {
|
||||
zIndex: -1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -64,31 +64,4 @@ describe('EditChannelInfo', () => {
|
||||
expect(instance.enableRightButton).toHaveBeenCalledTimes(1);
|
||||
expect(instance.enableRightButton).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should call scrollHeaderToTop', () => {
|
||||
const wrapper = shallow(
|
||||
<EditChannelInfo {...baseProps}/>
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.scrollHeaderToTop = jest.fn();
|
||||
|
||||
expect(instance.scrollHeaderToTop).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setState({keyboardVisible: false});
|
||||
instance.onHeaderFocus();
|
||||
expect(instance.scrollHeaderToTop).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setState({keyboardVisible: true});
|
||||
instance.onHeaderFocus();
|
||||
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setState({headerHasFocus: false});
|
||||
instance.onKeyboardDidShow();
|
||||
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setState({headerHasFocus: true});
|
||||
instance.onKeyboardDidShow();
|
||||
expect(instance.scrollHeaderToTop).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
@@ -39,8 +38,6 @@ export default class Emoji extends React.PureComponent {
|
||||
literal: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
unicode: PropTypes.string,
|
||||
customEmojiStyle: CustomPropTypes.Style,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -58,7 +55,7 @@ export default class Emoji extends React.PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentWillMount() {
|
||||
const {displayTextOnly, emojiName, imageUrl} = this.props;
|
||||
this.mounted = true;
|
||||
if (!displayTextOnly && imageUrl) {
|
||||
@@ -97,7 +94,6 @@ export default class Emoji extends React.PureComponent {
|
||||
literal,
|
||||
textStyle,
|
||||
displayTextOnly,
|
||||
customEmojiStyle,
|
||||
} = this.props;
|
||||
const {imageUrl} = this.state;
|
||||
|
||||
@@ -120,19 +116,6 @@ export default class Emoji extends React.PureComponent {
|
||||
// force a new image to be rendered when the size changes
|
||||
const key = Platform.OS === 'android' ? (height + '-' + width) : null;
|
||||
|
||||
if (this.props.unicode && !this.props.imageUrl) {
|
||||
const codeArray = this.props.unicode.split('-');
|
||||
const code = codeArray.reduce((acc, c) => {
|
||||
return acc + String.fromCodePoint(parseInt(c, 16));
|
||||
}, '');
|
||||
|
||||
return (
|
||||
<Text style={[this.props.textStyle, {fontSize: size}]}>
|
||||
{code}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return (
|
||||
<Image
|
||||
@@ -143,9 +126,9 @@ export default class Emoji extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
<Image
|
||||
key={key}
|
||||
style={[customEmojiStyle, {width, height}]}
|
||||
style={{width, height}}
|
||||
source={{uri: imageUrl}}
|
||||
onError={this.onError}
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
|
||||
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
||||
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
||||
|
||||
import Emoji from './emoji';
|
||||
|
||||
@@ -19,15 +19,11 @@ function mapStateToProps(state, ownProps) {
|
||||
const customEmojis = getCustomEmojisByName(state);
|
||||
|
||||
let imageUrl = '';
|
||||
let unicode;
|
||||
let isCustomEmoji = false;
|
||||
let displayTextOnly = false;
|
||||
if (EmojiIndicesByAlias.has(emojiName) || BuiltInEmojis.includes(emojiName)) {
|
||||
if (EmojiIndicesByAlias.has(emojiName)) {
|
||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
|
||||
unicode = emoji.filename;
|
||||
if (BuiltInEmojis.includes(emojiName)) {
|
||||
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
|
||||
}
|
||||
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
|
||||
} else if (customEmojis.has(emojiName)) {
|
||||
const emoji = customEmojis.get(emojiName);
|
||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
|
||||
@@ -37,6 +33,7 @@ function mapStateToProps(state, ownProps) {
|
||||
config.EnableCustomEmoji !== 'true' ||
|
||||
config.ExperimentalEnablePostMetadata === 'true' ||
|
||||
getCurrentUserId(state) === '' ||
|
||||
!isMinimumServerVersion(Client4.getServerVersion(), 4, 7) ||
|
||||
isMinimumServerVersion(Client4.getServerVersion(), 5, 12);
|
||||
}
|
||||
|
||||
@@ -44,7 +41,6 @@ function mapStateToProps(state, ownProps) {
|
||||
imageUrl,
|
||||
isCustomEmoji,
|
||||
displayTextOnly,
|
||||
unicode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
|
||||
disableVirtualization={false}
|
||||
getItemLayout={[Function]}
|
||||
horizontal={false}
|
||||
initialNumToRender={50}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -86,7 +86,7 @@ exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
|
||||
onMomentumScrollEnd={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollToIndexFailed={[Function]}
|
||||
pageSize={50}
|
||||
pageSize={30}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
|
||||
@@ -2,20 +2,61 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {
|
||||
FlatList,
|
||||
SectionList,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
|
||||
|
||||
import EmojiPickerBase, {getStyleSheetFromTheme} from './emoji_picker_base';
|
||||
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
|
||||
|
||||
export default class EmojiPicker extends EmojiPickerBase {
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {theme} = this.props;
|
||||
const {searchTerm} = this.state;
|
||||
const {deviceWidth, theme} = this.props;
|
||||
const {emojis, filteredEmojis, searchTerm} = this.state;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
let listComponent;
|
||||
if (searchTerm) {
|
||||
listComponent = (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps='always'
|
||||
style={styles.flatList}
|
||||
data={filteredEmojis}
|
||||
keyExtractor={this.flatListKeyExtractor}
|
||||
renderItem={this.flatListRenderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
listComponent = (
|
||||
<SectionList
|
||||
ref={this.attachSectionList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * 2)}]}
|
||||
sections={emojis}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='always'
|
||||
getItemLayout={this.sectionListGetItemLayout}
|
||||
removeClippedSubviews={true}
|
||||
onScroll={this.onScroll}
|
||||
onScrollToIndexFailed={this.handleScrollToSectionFailed}
|
||||
onMomentumScrollEnd={this.onMomentumScrollEnd}
|
||||
pageSize={30}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
onEndReached={this.loadMoreCustomEmojis}
|
||||
onEndReachedThreshold={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const searchBarInput = {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
color: theme.centerChannelColor,
|
||||
@@ -27,7 +68,7 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
<React.Fragment>
|
||||
<View style={styles.searchBar}>
|
||||
<SearchBar
|
||||
ref={this.setSearchBarRef}
|
||||
ref={this.searchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
@@ -46,7 +87,7 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
{this.renderListComponent(2)}
|
||||
{listComponent}
|
||||
{!searchTerm &&
|
||||
<View style={styles.bottomContentWrapper}>
|
||||
<View style={styles.bottomContent}>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
SectionList,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
|
||||
@@ -14,17 +16,58 @@ import SearchBar from 'app/components/search_bar';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from 'app/utils/theme';
|
||||
|
||||
import EmojiPickerBase, {getStyleSheetFromTheme, SCROLLVIEW_NATIVE_ID} from './emoji_picker_base';
|
||||
import EmojiPickerBase, {getStyleSheetFromTheme, SECTION_MARGIN} from './emoji_picker_base';
|
||||
|
||||
const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
|
||||
|
||||
export default class EmojiPicker extends EmojiPickerBase {
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {isLandscape, theme} = this.props;
|
||||
const {searchTerm} = this.state;
|
||||
const {deviceWidth, isLandscape, theme} = this.props;
|
||||
const {emojis, filteredEmojis, searchTerm} = this.state;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && isLandscape ? 6 : 2;
|
||||
|
||||
let listComponent;
|
||||
if (searchTerm) {
|
||||
listComponent = (
|
||||
<FlatList
|
||||
data={filteredEmojis}
|
||||
initialListSize={10}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.flatListKeyExtractor}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
pageSize={10}
|
||||
renderItem={this.flatListRenderItem}
|
||||
style={styles.flatList}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
listComponent = (
|
||||
<SectionList
|
||||
getItemLayout={this.sectionListGetItemLayout}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
ListFooterComponent={this.renderFooter}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
onEndReached={this.loadMoreCustomEmojis}
|
||||
onEndReachedThreshold={0}
|
||||
onMomentumScrollEnd={this.onMomentumScrollEnd}
|
||||
onScroll={this.onScroll}
|
||||
onScrollToIndexFailed={this.handleScrollToSectionFailed}
|
||||
pageSize={30}
|
||||
ref={this.attachSectionList}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
sections={emojis}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 50 : 30;
|
||||
if (isLandscape) {
|
||||
keyboardOffset = DeviceTypes.IS_IPHONE_WITH_INSETS ? 0 : 10;
|
||||
@@ -49,7 +92,7 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
>
|
||||
<View style={[styles.searchBar, padding(isLandscape)]}>
|
||||
<SearchBar
|
||||
ref={this.setSearchBarRef}
|
||||
ref={this.searchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
@@ -68,9 +111,10 @@ export default class EmojiPicker extends EmojiPickerBase {
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.container]}>
|
||||
{this.renderListComponent(shorten)}
|
||||
{listComponent}
|
||||
{!searchTerm &&
|
||||
<KeyboardTrackingView
|
||||
ref={this.keyboardTracker}
|
||||
scrollViewNativeID={SCROLLVIEW_NATIVE_ID}
|
||||
normalList={true}
|
||||
>
|
||||
|
||||
@@ -6,9 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Platform,
|
||||
SectionList,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -30,11 +28,10 @@ import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone
|
||||
import EmojiPickerRow from './emoji_picker_row';
|
||||
|
||||
const EMOJI_SIZE = 30;
|
||||
const EMOJI_GUTTER = 7;
|
||||
const EMOJI_GUTTER = 7.5;
|
||||
const EMOJIS_PER_PAGE = 200;
|
||||
const SECTION_HEADER_HEIGHT = 28;
|
||||
const SECTION_MARGIN = 15;
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiPicker';
|
||||
export const SECTION_MARGIN = 15;
|
||||
|
||||
export function filterEmojiSearchInput(searchText) {
|
||||
return searchText.toLowerCase().replace(/^:|:$/g, '');
|
||||
@@ -72,7 +69,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
this.sectionListGetItemLayout = sectionListGetItemLayout({
|
||||
getItemHeight: () => {
|
||||
return (EMOJI_SIZE + 7) + (EMOJI_GUTTER * 2);
|
||||
return EMOJI_SIZE + (EMOJI_GUTTER * 2);
|
||||
},
|
||||
getSectionHeaderHeight: () => SECTION_HEADER_HEIGHT,
|
||||
});
|
||||
@@ -80,6 +77,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
const emojis = this.renderableEmojis(props.emojisBySection, props.deviceWidth);
|
||||
const emojiSectionIndexByOffset = this.measureEmojiSections(emojis);
|
||||
|
||||
this.searchBarRef = React.createRef();
|
||||
this.scrollToSectionTries = 0;
|
||||
this.state = {
|
||||
emojis,
|
||||
@@ -96,8 +94,8 @@ export default class EmojiPicker extends PureComponent {
|
||||
if (this.props.deviceWidth !== nextProps.deviceWidth) {
|
||||
this.rebuildEmojis = true;
|
||||
|
||||
if (this.searchBarRef) {
|
||||
this.searchBarRef.blur();
|
||||
if (this.searchBarRef?.current) {
|
||||
this.searchBarRef.current.blur();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,14 +105,6 @@ export default class EmojiPicker extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
setSearchBarRef = (ref) => {
|
||||
this.searchBarRef = ref;
|
||||
}
|
||||
|
||||
setSectionListRef = (ref) => {
|
||||
this.sectionListRef = ref;
|
||||
};
|
||||
|
||||
setRebuiltEmojis = (searchBarAnimationComplete = true) => {
|
||||
if (this.rebuildEmojis && searchBarAnimationComplete) {
|
||||
this.rebuildEmojis = false;
|
||||
@@ -168,7 +158,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
let lastOffset = 0;
|
||||
return emojiSections.map((section) => {
|
||||
const start = lastOffset;
|
||||
const nextOffset = (section.data.length * ((EMOJI_SIZE + 7) + (EMOJI_GUTTER * 2))) + SECTION_HEADER_HEIGHT;
|
||||
const nextOffset = (section.data.length * (EMOJI_SIZE + (EMOJI_GUTTER * 2))) + SECTION_HEADER_HEIGHT;
|
||||
lastOffset += nextOffset;
|
||||
|
||||
return start;
|
||||
@@ -208,6 +198,10 @@ export default class EmojiPicker extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
filterEmojiAliases = (aliases, searchTerm) => {
|
||||
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
|
||||
};
|
||||
|
||||
searchEmojis = (searchTerm) => {
|
||||
const {emojis, fuse} = this.props;
|
||||
const searchTermLowerCase = searchTerm.toLowerCase();
|
||||
@@ -223,7 +217,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
|
||||
getNumberOfColumns = (deviceWidth) => {
|
||||
const shorten = DeviceTypes.IS_IPHONE_WITH_INSETS && this.props.isLandscape ? 4 : 2;
|
||||
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / ((EMOJI_SIZE + 7) + (EMOJI_GUTTER * shorten)))));
|
||||
return Math.floor(Number(((deviceWidth - (SECTION_MARGIN * shorten)) / (EMOJI_SIZE + (EMOJI_GUTTER * shorten)))));
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
@@ -238,54 +232,6 @@ export default class EmojiPicker extends PureComponent {
|
||||
);
|
||||
};
|
||||
|
||||
renderListComponent = (shorten) => {
|
||||
const {deviceWidth, theme} = this.props;
|
||||
const {emojis, filteredEmojis, searchTerm} = this.state;
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
|
||||
let listComponent;
|
||||
if (searchTerm) {
|
||||
listComponent = (
|
||||
<FlatList
|
||||
data={filteredEmojis}
|
||||
initialListSize={10}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.flatListKeyExtractor}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
pageSize={10}
|
||||
renderItem={this.flatListRenderItem}
|
||||
style={styles.flatList}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
listComponent = (
|
||||
<SectionList
|
||||
ref={this.setSectionListRef}
|
||||
getItemLayout={this.sectionListGetItemLayout}
|
||||
initialNumToRender={50}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
ListFooterComponent={this.renderFooter}
|
||||
nativeID={SCROLLVIEW_NATIVE_ID}
|
||||
onEndReached={this.loadMoreCustomEmojis}
|
||||
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
|
||||
onMomentumScrollEnd={this.onMomentumScrollEnd}
|
||||
onScroll={this.onScroll}
|
||||
onScrollToIndexFailed={this.handleScrollToSectionFailed}
|
||||
pageSize={50}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
sections={emojis}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[styles.sectionList, {width: deviceWidth - (SECTION_MARGIN * shorten)}]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return listComponent;
|
||||
};
|
||||
|
||||
flatListKeyExtractor = (item) => item;
|
||||
|
||||
flatListRenderItem = ({item}) => {
|
||||
@@ -325,7 +271,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
}
|
||||
|
||||
this.props.actions.incrementEmojiPickerPage();
|
||||
};
|
||||
}
|
||||
|
||||
onScroll = (e) => {
|
||||
if (this.state.jumpToSection) {
|
||||
@@ -363,7 +309,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
jumpToSection: true,
|
||||
currentSectionIndex: index,
|
||||
}, () => {
|
||||
this.sectionListRef.scrollToLocation({
|
||||
this.sectionList.scrollToLocation({
|
||||
sectionIndex: index,
|
||||
itemIndex: 0,
|
||||
viewOffset: 25,
|
||||
@@ -378,7 +324,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
this.scrollToSection(index);
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme} = this.props;
|
||||
@@ -405,7 +351,7 @@ export default class EmojiPicker extends PureComponent {
|
||||
if (isCustomSection && this.props.customEmojiPage === 0) {
|
||||
this.loadMoreCustomEmojis();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderSectionIcons = () => {
|
||||
const {theme} = this.props;
|
||||
@@ -430,6 +376,10 @@ export default class EmojiPicker extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
attachSectionList = (c) => {
|
||||
this.sectionList = c;
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
if (!this.state.missingPages) {
|
||||
return null;
|
||||
@@ -440,10 +390,10 @@ export default class EmojiPicker extends PureComponent {
|
||||
const styles = getStyleSheetFromTheme(theme);
|
||||
return (
|
||||
<View style={styles.loading}>
|
||||
<ActivityIndicator color={theme.centerChannelColor}/>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
|
||||
@@ -27,12 +27,11 @@ export default class EmojiPickerRow extends Component {
|
||||
renderEmojis = (emoji, index, emojis) => {
|
||||
const {emojiGutter, emojiSize} = this.props;
|
||||
|
||||
const size = emojiSize + 7;
|
||||
const style = [
|
||||
styles.emoji,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
width: emojiSize,
|
||||
height: emojiSize,
|
||||
marginHorizontal: emojiGutter,
|
||||
},
|
||||
];
|
||||
@@ -61,7 +60,6 @@ export default class EmojiPickerRow extends Component {
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emoji.name}
|
||||
textStyle={styles.emojiText}
|
||||
size={emojiSize}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -81,7 +79,7 @@ export default class EmojiPickerRow extends Component {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
columnStyle: {
|
||||
flex: 1,
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
@@ -90,10 +88,6 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
emojiText: {
|
||||
color: '#000',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
emojiLeft: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emoj
|
||||
|
||||
import {incrementEmojiPickerPage} from 'app/actions/views/emoji';
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
import {BuiltInEmojis, CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
import {CategoryNames, Emojis, EmojiIndicesByAlias, EmojiIndicesByCategory} from 'app/utils/emojis';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import EmojiPicker from './emoji_picker';
|
||||
@@ -96,11 +96,6 @@ const getEmojisBySection = createSelector(
|
||||
});
|
||||
|
||||
const customEmojiItems = [];
|
||||
BuiltInEmojis.forEach((emoji) => {
|
||||
customEmojiItems.push({
|
||||
name: emoji,
|
||||
});
|
||||
});
|
||||
|
||||
for (const [key] of customEmojis) {
|
||||
customEmojiItems.push({
|
||||
|
||||
@@ -5,70 +5,76 @@ exports[`FileAttachment should match snapshot 1`] = `
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.4)",
|
||||
"borderRadius": 5,
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 2,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginRight": 10,
|
||||
"marginTop": 10,
|
||||
"width": 300,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
type="opacity"
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
backgroundColor="#fff"
|
||||
file={
|
||||
Object {
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
}
|
||||
iconHeight={60}
|
||||
iconWidth={60}
|
||||
onCaptureRef={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
wrapperHeight={80}
|
||||
wrapperWidth={80}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginHorizontal": 20,
|
||||
"marginVertical": 10,
|
||||
"borderLeftColor": "rgba(61,60,64,0.2)",
|
||||
"borderLeftWidth": 1,
|
||||
"flex": 1,
|
||||
"paddingHorizontal": 8,
|
||||
"paddingVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
type="opacity"
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={
|
||||
Object {
|
||||
"mime_type": "image/png",
|
||||
}
|
||||
}
|
||||
iconHeight={48}
|
||||
iconWidth={36}
|
||||
onCaptureRef={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
wrapperHeight={48}
|
||||
wrapperWidth={36}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</View>
|
||||
type="opacity"
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,18 +4,14 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Dimensions,
|
||||
PixelRatio,
|
||||
Text,
|
||||
View,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isDocument, isGif} from 'app/utils/file';
|
||||
import {calculateDimensions} from 'app/utils/images';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FileAttachmentDocument from './file_attachment_document';
|
||||
@@ -32,14 +28,10 @@ export default class FileAttachment extends PureComponent {
|
||||
onLongPress: PropTypes.func,
|
||||
onPreviewPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
wrapperWidth: PropTypes.number,
|
||||
isSingleImage: PropTypes.bool,
|
||||
nonVisibleImagesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onPreviewPress: () => true,
|
||||
wrapperWidth: 300,
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
@@ -59,7 +51,7 @@ export default class FileAttachment extends PureComponent {
|
||||
};
|
||||
|
||||
renderFileInfo() {
|
||||
const {file, onLongPress, theme} = this.props;
|
||||
const {file, theme} = this.props;
|
||||
const {data} = file;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
@@ -68,31 +60,24 @@ export default class FileAttachment extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
style={style.attachmentContainer}
|
||||
>
|
||||
<React.Fragment>
|
||||
<View style={style.attachmentContainer}>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileName}
|
||||
>
|
||||
{file.caption.trim()}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileName}
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{file.caption.trim()}
|
||||
{`${data.extension.toUpperCase()} ${Utils.getFormattedFileSize(data)}`}
|
||||
</Text>
|
||||
<View style={style.fileDownloadContainer}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.fileInfo}
|
||||
>
|
||||
{`${Utils.getFormattedFileSize(data)}`}
|
||||
</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,118 +85,75 @@ export default class FileAttachment extends PureComponent {
|
||||
this.documentElement = ref;
|
||||
};
|
||||
|
||||
renderMoreImagesOverlay = (value) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={style.moreImagesWrapper}>
|
||||
<Text style={style.moreImagesText}>
|
||||
{`+${value}`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
getImageDimensions = (file) => {
|
||||
const {isSingleImage, wrapperWidth} = this.props;
|
||||
const viewPortHeight = this.getViewPortHeight();
|
||||
|
||||
if (isSingleImage) {
|
||||
return calculateDimensions(file?.height, file?.width, wrapperWidth, viewPortHeight);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getViewPortHeight = () => {
|
||||
const dimensions = Dimensions.get('window');
|
||||
const viewPortHeight = Math.max(dimensions.height, dimensions.width) * 0.45;
|
||||
|
||||
return viewPortHeight;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
canDownloadFiles,
|
||||
file,
|
||||
theme,
|
||||
onLongPress,
|
||||
isSingleImage,
|
||||
nonVisibleImagesCount,
|
||||
} = this.props;
|
||||
const {data} = file;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if ((data && data.has_preview_image) || file.loading || isGif(data)) {
|
||||
const imageDimensions = this.getImageDimensions(data);
|
||||
|
||||
fileAttachmentComponent = (
|
||||
<TouchableWithFeedback
|
||||
key={`${this.props.id}${file.loading}`}
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
style={{width: imageDimensions?.width}}
|
||||
>
|
||||
<FileAttachmentImage
|
||||
file={data || {}}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
imageDimensions={imageDimensions}
|
||||
/>
|
||||
{this.renderMoreImagesOverlay(nonVisibleImagesCount, theme)}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
} else if (isDocument(data)) {
|
||||
fileAttachmentComponent = (
|
||||
<View style={[style.fileWrapper]}>
|
||||
<View style={style.iconWrapper}>
|
||||
<FileAttachmentDocument
|
||||
ref={this.setDocumentRef}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
{this.renderFileInfo()}
|
||||
</View>
|
||||
<FileAttachmentDocument
|
||||
ref={this.setDocumentRef}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={file}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = (
|
||||
<View style={[style.fileWrapper]}>
|
||||
<View style={style.iconWrapper}>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
{this.renderFileInfo()}
|
||||
</View>
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
<FileAttachmentIcon
|
||||
file={data}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
theme={theme}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
return fileAttachmentComponent;
|
||||
return (
|
||||
<View style={[style.fileWrapper]}>
|
||||
{fileAttachmentComponent}
|
||||
<TouchableWithFeedback
|
||||
style={style.fileInfoContainer}
|
||||
onLongPress={onLongPress}
|
||||
onPress={this.handlePreviewPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
{this.renderFileInfo()}
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const scale = Dimensions.get('window').width / 320;
|
||||
|
||||
return {
|
||||
attachmentContainer: {
|
||||
flex: 1,
|
||||
@@ -226,28 +168,33 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginTop: 3,
|
||||
},
|
||||
fileInfo: {
|
||||
marginLeft: 2,
|
||||
fontSize: 14,
|
||||
color: theme.centerChannelColor,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
fileInfoContainer: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 5,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
},
|
||||
fileName: {
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
marginLeft: 2,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: theme.centerChannelColor,
|
||||
paddingRight: 10,
|
||||
},
|
||||
fileWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 10,
|
||||
marginRight: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
borderRadius: 5,
|
||||
},
|
||||
iconWrapper: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: 10,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderRadius: 2,
|
||||
width: 300,
|
||||
},
|
||||
circularProgress: {
|
||||
width: '100%',
|
||||
@@ -264,18 +211,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
moreImagesWrapper: {
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
borderRadius: 5,
|
||||
},
|
||||
moreImagesText: {
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
fontSize: Math.round(PixelRatio.roundToNearestPixel(24 * scale)),
|
||||
fontFamily: 'Open Sans',
|
||||
textAlign: 'center',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import OpenFile from 'react-native-doc-viewer';
|
||||
@@ -28,9 +27,8 @@ import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
|
||||
|
||||
const {DOCUMENTS_PATH} = DeviceTypes;
|
||||
const DOWNLOADING_OFFSET = 28;
|
||||
const TEXT_PREVIEW_FORMATS = [
|
||||
'application/json',
|
||||
'application/x-x509-ca-cert',
|
||||
@@ -52,10 +50,10 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
iconWidth: ATTACHMENT_ICON_WIDTH,
|
||||
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
wrapperWidth: ATTACHMENT_ICON_WIDTH,
|
||||
iconHeight: 47,
|
||||
iconWidth: 47,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -71,7 +69,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.eventEmitter = new NativeEventEmitter(NativeModules.RNDocViewer);
|
||||
this.eventEmitter = new NativeEventEmitter(NativeModules.RNReactNativeDocViewer);
|
||||
this.eventEmitter.addListener('DoneButtonEvent', this.onDonePreviewingFile);
|
||||
}
|
||||
|
||||
@@ -286,6 +284,16 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
renderProgress = () => {
|
||||
const {wrapperWidth} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[style.circularProgressContent, {width: wrapperWidth}]}>
|
||||
{this.renderFileAttachmentIcon()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
showDownloadDisabledAlert = () => {
|
||||
const {intl} = this.context;
|
||||
|
||||
@@ -330,6 +338,14 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
renderFileAttachmentIcon = () => {
|
||||
const {backgroundColor, iconHeight, iconWidth, file, theme, wrapperHeight, wrapperWidth} = this.props;
|
||||
const {downloading} = this.state;
|
||||
let height = wrapperHeight;
|
||||
let width = wrapperWidth;
|
||||
|
||||
if (downloading) {
|
||||
height -= DOWNLOADING_OFFSET;
|
||||
width -= DOWNLOADING_OFFSET;
|
||||
}
|
||||
|
||||
return (
|
||||
<FileAttachmentIcon
|
||||
@@ -338,39 +354,29 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
theme={theme}
|
||||
iconHeight={iconHeight}
|
||||
iconWidth={iconWidth}
|
||||
wrapperHeight={wrapperHeight}
|
||||
wrapperWidth={wrapperWidth}
|
||||
wrapperHeight={height}
|
||||
wrapperWidth={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDownloadProgres = () => {
|
||||
const {theme} = this.props;
|
||||
return (
|
||||
<Text style={{fontSize: 10, color: theme.centerChannelColor, fontWeight: '600'}}>
|
||||
{`${this.state.progress}%`}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {onLongPress, theme} = this.props;
|
||||
const {onLongPress, theme, wrapperHeight} = this.props;
|
||||
const {downloading, progress} = this.state;
|
||||
|
||||
let fileAttachmentComponent;
|
||||
if (downloading) {
|
||||
fileAttachmentComponent = (
|
||||
<View style={[style.circularProgressContent]}>
|
||||
<CircularProgress
|
||||
size={40}
|
||||
fill={progress}
|
||||
width={circularProgressWidth}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderDownloadProgres}
|
||||
</CircularProgress>
|
||||
</View>
|
||||
<CircularProgress
|
||||
size={wrapperHeight}
|
||||
fill={progress}
|
||||
width={circularProgressWidth}
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColor={theme.linkColor}
|
||||
rotation={0}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</CircularProgress>
|
||||
);
|
||||
} else {
|
||||
fileAttachmentComponent = this.renderFileAttachmentIcon();
|
||||
@@ -390,9 +396,11 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
|
||||
const style = StyleSheet.create({
|
||||
circularProgressContent: {
|
||||
left: -(circularProgressWidth - 2),
|
||||
top: 4,
|
||||
width: 36,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: -circularProgressWidth,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,13 +19,9 @@ import imageIcon from 'assets/images/icons/image.png';
|
||||
import patchIcon from 'assets/images/icons/patch.png';
|
||||
import pdfIcon from 'assets/images/icons/pdf.png';
|
||||
import pptIcon from 'assets/images/icons/ppt.png';
|
||||
import textIcon from 'assets/images/icons/text.png';
|
||||
import videoIcon from 'assets/images/icons/video.png';
|
||||
import wordIcon from 'assets/images/icons/word.png';
|
||||
|
||||
import {ATTACHMENT_ICON_HEIGHT, ATTACHMENT_ICON_WIDTH} from 'app/constants/attachment';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const ICON_PATH_FROM_FILE_TYPE = {
|
||||
audio: audioIcon,
|
||||
code: codeIcon,
|
||||
@@ -35,7 +31,6 @@ const ICON_PATH_FROM_FILE_TYPE = {
|
||||
pdf: pdfIcon,
|
||||
presentation: pptIcon,
|
||||
spreadsheet: excelIcon,
|
||||
text: textIcon,
|
||||
video: videoIcon,
|
||||
word: wordIcon,
|
||||
};
|
||||
@@ -49,14 +44,14 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
onCaptureRef: PropTypes.func,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
iconHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
iconWidth: ATTACHMENT_ICON_WIDTH,
|
||||
wrapperHeight: ATTACHMENT_ICON_HEIGHT,
|
||||
wrapperWidth: ATTACHMENT_ICON_WIDTH,
|
||||
backgroundColor: '#fff',
|
||||
iconHeight: 60,
|
||||
iconWidth: 60,
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
};
|
||||
|
||||
getFileIconPath(file) {
|
||||
@@ -73,17 +68,16 @@ export default class FileAttachmentIcon extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth, theme} = this.props;
|
||||
const {backgroundColor, file, iconHeight, iconWidth, wrapperHeight, wrapperWidth} = this.props;
|
||||
const source = this.getFileIconPath(file);
|
||||
const bgColor = backgroundColor || theme.centerChannelBg || 'transparent';
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[styles.fileIconWrapper, {backgroundColor: bgColor, height: wrapperHeight, width: wrapperWidth}]}
|
||||
style={[styles.fileIconWrapper, {backgroundColor, height: wrapperHeight, width: wrapperWidth}]}
|
||||
>
|
||||
<Image
|
||||
style={{maxHeight: iconHeight, maxWidth: iconWidth, tintColor: changeOpacity(theme.centerChannelColor, 20)}}
|
||||
style={[styles.icon, {height: iconHeight, width: iconWidth}]}
|
||||
source={source}
|
||||
/>
|
||||
</View>
|
||||
@@ -95,5 +89,12 @@ const styles = StyleSheet.create({
|
||||
fileIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
},
|
||||
icon: {
|
||||
borderTopLeftRadius: 2,
|
||||
borderBottomLeftRadius: 2,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -15,13 +15,9 @@ import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {isGif} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
|
||||
const SMALL_IMAGE_MAX_HEIGHT = 48;
|
||||
const SMALL_IMAGE_MAX_WIDTH = 48;
|
||||
|
||||
const IMAGE_SIZE = {
|
||||
Fullsize: 'fullsize',
|
||||
Preview: 'preview',
|
||||
@@ -39,18 +35,22 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
]),
|
||||
imageWidth: PropTypes.number,
|
||||
onCaptureRef: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
resizeMode: PropTypes.string,
|
||||
resizeMethod: PropTypes.string,
|
||||
wrapperHeight: PropTypes.number,
|
||||
wrapperWidth: PropTypes.number,
|
||||
isSingleImage: PropTypes.bool,
|
||||
imageDimensions: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
fadeInOnLoad: false,
|
||||
imageHeight: 80,
|
||||
imageSize: IMAGE_SIZE.Preview,
|
||||
imageWidth: 80,
|
||||
loading: false,
|
||||
resizeMode: 'cover',
|
||||
resizeMethod: 'resize',
|
||||
wrapperHeight: 80,
|
||||
wrapperWidth: 80,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -72,11 +72,15 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
boxPlaceholder = () => {
|
||||
if (this.props.isSingleImage) {
|
||||
return null;
|
||||
calculateNeededWidth = (height, width, newHeight) => {
|
||||
const ratio = width / height;
|
||||
|
||||
let newWidth = newHeight * ratio;
|
||||
if (newWidth < newHeight) {
|
||||
newWidth = newHeight;
|
||||
}
|
||||
return (<View style={style.boxPlaceholder}/>);
|
||||
|
||||
return newWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref) => {
|
||||
@@ -87,7 +91,27 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
imageProps = (file) => {
|
||||
render() {
|
||||
const {
|
||||
file,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
imageSize,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
wrapperHeight,
|
||||
wrapperWidth,
|
||||
} = this.props;
|
||||
|
||||
let height = imageHeight;
|
||||
let width = imageWidth;
|
||||
let imageStyle = {height, width};
|
||||
if (imageSize === IMAGE_SIZE.Preview) {
|
||||
height = 80;
|
||||
width = this.calculateNeededWidth(file.height, file.width, height) || 80;
|
||||
imageStyle = {height, width, position: 'absolute', top: 0, left: 0, borderBottomLeftRadius: 2, borderTopLeftRadius: 2};
|
||||
}
|
||||
|
||||
const imageProps = {};
|
||||
if (file.localPath) {
|
||||
imageProps.defaultSource = {uri: file.localPath};
|
||||
@@ -95,73 +119,20 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
|
||||
imageProps.imageUri = Client4.getFilePreviewUrl(file.id);
|
||||
}
|
||||
return imageProps;
|
||||
};
|
||||
|
||||
renderSmallImage = () => {
|
||||
const {file, isSingleImage, resizeMethod, theme} = this.props;
|
||||
|
||||
let wrapperStyle = style.fileImageWrapper;
|
||||
|
||||
if (isSingleImage) {
|
||||
wrapperStyle = style.singleSmallImageWrapper;
|
||||
|
||||
if (file.width > SMALL_IMAGE_MAX_WIDTH) {
|
||||
wrapperStyle = [wrapperStyle, {width: '100%'}];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={[
|
||||
wrapperStyle,
|
||||
style.smallImageBorder,
|
||||
{borderColor: changeOpacity(theme.centerChannelColor, 0.4)},
|
||||
]}
|
||||
style={[style.fileImageWrapper, {height: wrapperHeight, width: wrapperWidth, overflow: 'hidden'}]}
|
||||
>
|
||||
{this.boxPlaceholder()}
|
||||
<View style={style.smallImageOverlay}>
|
||||
<ProgressiveImage
|
||||
style={{height: file.height, width: file.width}}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
filename={file.name}
|
||||
resizeMode={'contain'}
|
||||
resizeMethod={resizeMethod}
|
||||
{...this.imageProps(file)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
file,
|
||||
imageDimensions,
|
||||
resizeMethod,
|
||||
resizeMode,
|
||||
} = this.props;
|
||||
|
||||
if (file.height <= SMALL_IMAGE_MAX_HEIGHT || file.width <= SMALL_IMAGE_MAX_WIDTH) {
|
||||
return this.renderSmallImage();
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.handleCaptureRef}
|
||||
style={style.fileImageWrapper}
|
||||
>
|
||||
{this.boxPlaceholder()}
|
||||
<ProgressiveImage
|
||||
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
|
||||
style={imageStyle}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
filename={file.name}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...this.imageProps(file)}
|
||||
{...imageProps}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -169,28 +140,17 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
imagePreview: {
|
||||
...StyleSheet.absoluteFill,
|
||||
},
|
||||
fileImageWrapper: {
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
boxPlaceholder: {
|
||||
paddingBottom: '100%',
|
||||
},
|
||||
smallImageBorder: {
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
},
|
||||
smallImageOverlay: {
|
||||
...StyleSheet.absoluteFill,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderBottomLeftRadius: 2,
|
||||
borderTopLeftRadius: 2,
|
||||
},
|
||||
singleSmallImageWrapper: {
|
||||
height: SMALL_IMAGE_MAX_HEIGHT,
|
||||
width: SMALL_IMAGE_MAX_WIDTH,
|
||||
overflow: 'hidden',
|
||||
loaderContainer: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {isDocument, isGif, isVideo} from 'app/utils/file';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {previewImageAtIndex} from 'app/utils/images';
|
||||
@@ -20,11 +18,7 @@ import {emptyFunction} from 'app/utils/general';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
const MAX_VISIBLE_ROW_IMAGES = 4;
|
||||
const VIEWPORT_IMAGE_OFFSET = 70;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
|
||||
export default class FileAttachmentList extends PureComponent {
|
||||
export default class FileAttachmentList extends Component {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
|
||||
@@ -36,7 +30,6 @@ export default class FileAttachmentList extends PureComponent {
|
||||
onLongPress: PropTypes.func,
|
||||
postId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
isReplyPost: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -47,25 +40,19 @@ export default class FileAttachmentList extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.items = [];
|
||||
this.filesForGallery = this.getFilesForGallery(props);
|
||||
this.previewItems = [];
|
||||
|
||||
this.state = {
|
||||
loadingFiles: props.files.length === 0,
|
||||
};
|
||||
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
this.buildGalleryFiles(props).then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {files} = this.props;
|
||||
|
||||
this.mounted = true;
|
||||
this.handlePermanentSidebar();
|
||||
this.handleDimensions();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
|
||||
if (files.length === 0) {
|
||||
this.loadFilesForPost();
|
||||
}
|
||||
@@ -73,8 +60,7 @@ export default class FileAttachmentList extends PureComponent {
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.files !== nextProps.files) {
|
||||
this.filesForGallery = this.getFilesForGallery(nextProps);
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
this.buildGalleryFiles(nextProps).then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
@@ -86,33 +72,20 @@ export default class FileAttachmentList extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
loadFilesForPost = async () => {
|
||||
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
|
||||
this.setState({
|
||||
loadingFiles: false,
|
||||
});
|
||||
}
|
||||
|
||||
attachmentIndex = (fileId) => {
|
||||
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
|
||||
attachmentManifest = (attachments) => {
|
||||
return attachments.reduce((info, file) => {
|
||||
if (this.isImage(file)) {
|
||||
info.imageAttachments.push(file);
|
||||
} else {
|
||||
info.nonImageAttachments.push(file);
|
||||
}
|
||||
return info;
|
||||
}, {imageAttachments: [], nonImageAttachments: []});
|
||||
};
|
||||
|
||||
buildGalleryFiles = async () => {
|
||||
buildGalleryFiles = async (props) => {
|
||||
const {files} = props;
|
||||
const results = [];
|
||||
|
||||
if (this.filesForGallery && this.filesForGallery.length) {
|
||||
for (let i = 0; i < this.filesForGallery.length; i++) {
|
||||
const file = this.filesForGallery[i];
|
||||
if (files && files.length) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const caption = file.name;
|
||||
|
||||
if (isDocument(file) || isVideo(file) || (!file.has_preview_image && !isGif(file))) {
|
||||
@@ -143,141 +116,16 @@ export default class FileAttachmentList extends PureComponent {
|
||||
return results;
|
||||
};
|
||||
|
||||
getFilesForGallery = (props) => {
|
||||
const manifest = this.attachmentManifest(props.files);
|
||||
const files = manifest.imageAttachments.concat(manifest.nonImageAttachments);
|
||||
const results = [];
|
||||
|
||||
if (files && files.length) {
|
||||
files.forEach((file) => {
|
||||
results.push(file);
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
getPortraitPostWidth = () => {
|
||||
const {isReplyPost} = this.props;
|
||||
const {width, height} = Dimensions.get('window');
|
||||
const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
if (permanentSidebar) {
|
||||
portraitPostWidth -= TABLET_WIDTH;
|
||||
}
|
||||
|
||||
if (isReplyPost) {
|
||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||
}
|
||||
|
||||
return portraitPostWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref, idx) => {
|
||||
this.items[idx] = ref;
|
||||
};
|
||||
|
||||
handleDimensions = () => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = preventDoubleTap((idx) => {
|
||||
previewImageAtIndex(this.items, idx, this.galleryFiles);
|
||||
});
|
||||
|
||||
isImage = (file) => (file.has_preview_image || isGif(file));
|
||||
|
||||
isSingleImage = (files) => (files.length === 1 && this.isImage(files[0]));
|
||||
|
||||
loadFilesForPost = async () => {
|
||||
await this.props.actions.loadFilesForPostIfNecessary(this.props.postId);
|
||||
this.setState({
|
||||
loadingFiles: false,
|
||||
});
|
||||
}
|
||||
|
||||
renderItems = (items, moreImagesCount, includeGutter = false) => {
|
||||
const {canDownloadFiles, onLongPress, theme} = this.props;
|
||||
const isSingleImage = this.isSingleImage(items);
|
||||
let nonVisibleImagesCount;
|
||||
let container = styles.container;
|
||||
const containerWithGutter = [container, styles.gutter];
|
||||
|
||||
return items.map((file, idx) => {
|
||||
const f = {
|
||||
caption: file.name,
|
||||
data: file,
|
||||
};
|
||||
|
||||
if (moreImagesCount && idx === MAX_VISIBLE_ROW_IMAGES - 1) {
|
||||
nonVisibleImagesCount = moreImagesCount;
|
||||
}
|
||||
|
||||
if (idx !== 0 && includeGutter) {
|
||||
container = containerWithGutter;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={container}
|
||||
key={file.id}
|
||||
>
|
||||
<FileAttachment
|
||||
key={file.id}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={f}
|
||||
id={file.id}
|
||||
index={this.attachmentIndex(file.id)}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
onLongPress={onLongPress}
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
wrapperWidth={this.getPortraitPostWidth()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
renderImageRow = (images) => {
|
||||
if (images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES);
|
||||
const {portraitPostWidth} = this.state;
|
||||
|
||||
let nonVisibleImagesCount;
|
||||
if (images.length > MAX_VISIBLE_ROW_IMAGES) {
|
||||
nonVisibleImagesCount = images.length - MAX_VISIBLE_ROW_IMAGES;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.row, {width: portraitPostWidth}]}>
|
||||
{ this.renderItems(visibleImages, nonVisibleImagesCount, true) }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {canDownloadFiles, fileIds, files, isFailed} = this.props;
|
||||
renderItems = () => {
|
||||
const {canDownloadFiles, fileIds, files} = this.props;
|
||||
|
||||
if (!files.length && fileIds.length > 0) {
|
||||
return fileIds.map((id, idx) => (
|
||||
@@ -292,29 +140,45 @@ export default class FileAttachmentList extends PureComponent {
|
||||
));
|
||||
}
|
||||
|
||||
const manifest = this.attachmentManifest(files);
|
||||
return files.map((file, idx) => {
|
||||
const f = {
|
||||
caption: file.name,
|
||||
data: file,
|
||||
};
|
||||
|
||||
return (
|
||||
<FileAttachment
|
||||
key={file.id}
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
file={f}
|
||||
id={file.id}
|
||||
index={idx}
|
||||
onCaptureRef={this.handleCaptureRef}
|
||||
onPreviewPress={this.handlePreviewPress}
|
||||
onLongPress={this.props.onLongPress}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {fileIds, isFailed} = this.props;
|
||||
|
||||
return (
|
||||
<View style={[isFailed && styles.failed]}>
|
||||
{this.renderImageRow(manifest.imageAttachments)}
|
||||
{this.renderItems(manifest.nonImageAttachments)}
|
||||
</View>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
scrollEnabled={fileIds.length > 1}
|
||||
style={[(isFailed && styles.failed)]}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
>
|
||||
{this.renderItems()}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginTop: 5,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gutter: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
failed: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
|
||||
@@ -10,50 +10,9 @@ jest.mock('react-native-doc-viewer', () => ({
|
||||
openDoc: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('FileAttachmentList', () => {
|
||||
describe('PostAttachmentOpenGraph', () => {
|
||||
const loadFilesForPostIfNecessary = jest.fn().mockImplementationOnce(() => Promise.resolve({data: {}}));
|
||||
|
||||
const files = [{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 171,
|
||||
id: 'fileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image01.png',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 425,
|
||||
},
|
||||
{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 800,
|
||||
id: 'otherFileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image02.png',
|
||||
post_id: 'postId',
|
||||
size: 24894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 555,
|
||||
}];
|
||||
|
||||
const nonImage = {
|
||||
extension: 'other',
|
||||
id: 'fileId',
|
||||
mime_type: 'other/type',
|
||||
name: 'file01.other',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
user_id: 'userId',
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
actions: {
|
||||
loadFilesForPostIfNecessary,
|
||||
@@ -62,7 +21,21 @@ describe('FileAttachmentList', () => {
|
||||
deviceHeight: 680,
|
||||
deviceWidth: 660,
|
||||
fileIds: ['fileId'],
|
||||
files: [files[0]],
|
||||
files: [{
|
||||
create_at: 1546893090093,
|
||||
delete_at: 0,
|
||||
extension: 'png',
|
||||
has_preview_image: true,
|
||||
height: 171,
|
||||
id: 'fileId',
|
||||
mime_type: 'image/png',
|
||||
name: 'image.png',
|
||||
post_id: 'postId',
|
||||
size: 14894,
|
||||
update_at: 1546893090093,
|
||||
user_id: 'userId',
|
||||
width: 425,
|
||||
}],
|
||||
postId: 'postId',
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
@@ -75,93 +48,6 @@ describe('FileAttachmentList', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with two image files', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with three image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with four image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage, fourthImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with more than four image files', () => {
|
||||
const thirdImage = {...files[1], id: 'thirdFileId', name: 'image03.png'};
|
||||
const fourthImage = {...files[1], id: 'fourthFileId', name: 'image04.png'};
|
||||
const fifthImage = {...files[1], id: 'fifthFileId', name: 'image05.png'};
|
||||
const sixthImage = {...files[1], id: 'sixthFileId', name: 'image06.png'};
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, thirdImage, fourthImage, fifthImage, sixthImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with non-image attachment', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [nonImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with combination of image and non-image attachments', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
files: [...files, nonImage],
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...props}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call loadFilesForPostIfNecessary when files does not exist', async () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
|
||||
@@ -190,7 +190,11 @@ export default class FileUploadItem extends PureComponent {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
imageSize='fullsize'
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -198,6 +202,8 @@ export default class FileUploadItem extends PureComponent {
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
imageHeight={100}
|
||||
imageWidth={100}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
@@ -254,7 +260,6 @@ const styles = StyleSheet.create({
|
||||
height: 100,
|
||||
width: 100,
|
||||
elevation: 10,
|
||||
borderRadius: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
backgroundColor: '#fff',
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class LoadMorePosts extends PureComponent {
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, alignItems: 'center'}}>
|
||||
<ActivityIndicator color={this.props.theme.centerChannelColor}/>
|
||||
<ActivityIndicator/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,14 +92,6 @@ export default class MarkdownImage extends React.Component {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
setImageRef = (ref) => {
|
||||
this.imageRef = ref;
|
||||
}
|
||||
|
||||
setItemRef = (ref) => {
|
||||
this.itemRef = ref;
|
||||
}
|
||||
|
||||
getSource = () => {
|
||||
let source = this.props.source;
|
||||
|
||||
@@ -203,7 +195,7 @@ export default class MarkdownImage extends React.Component {
|
||||
},
|
||||
}];
|
||||
|
||||
previewImageAtIndex([this.itemRef], 0, files);
|
||||
previewImageAtIndex([this.refs.item], 0, files);
|
||||
};
|
||||
|
||||
loadImageSize = (source) => {
|
||||
@@ -260,7 +252,7 @@ export default class MarkdownImage extends React.Component {
|
||||
style={{width, height}}
|
||||
>
|
||||
<ProgressiveImage
|
||||
ref={this.setImageRef}
|
||||
ref='image'
|
||||
defaultSource={source}
|
||||
resizeMode='contain'
|
||||
style={{width, height}}
|
||||
@@ -290,7 +282,7 @@ export default class MarkdownImage extends React.Component {
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
ref='item'
|
||||
style={style.container}
|
||||
>
|
||||
{image}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MarkdownTable should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<ScrollViewMock
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"width": 1000,
|
||||
}
|
||||
}
|
||||
onContentSizeChange={[Function]}
|
||||
onLayout={[Function]}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderBottomWidth": 1,
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRightWidth": 1,
|
||||
"maxHeight": 300,
|
||||
},
|
||||
Object {
|
||||
"maxWidth": 1000,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderLeftWidth": 1,
|
||||
"borderTopWidth": 1,
|
||||
"width": "100%",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<
|
||||
className="row"
|
||||
>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
</>
|
||||
<
|
||||
className="row"
|
||||
>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
</>
|
||||
<
|
||||
className="row"
|
||||
>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
</>
|
||||
<
|
||||
className="row"
|
||||
>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
</>
|
||||
<
|
||||
className="row"
|
||||
isLastRow={true}
|
||||
>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
<
|
||||
className="col"
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
</ScrollViewMock>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"height": 34,
|
||||
"left": -20,
|
||||
"width": 34,
|
||||
}
|
||||
}
|
||||
type="native"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "flex-end",
|
||||
"maxWidth": "100%",
|
||||
"paddingBottom": 4,
|
||||
"paddingRight": 14,
|
||||
"paddingTop": 8,
|
||||
},
|
||||
Object {
|
||||
"width": 1000,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderColor": "rgba(61,60,64,0.2)",
|
||||
"borderRadius": 50,
|
||||
"borderWidth": 1,
|
||||
"height": 34,
|
||||
"justifyContent": "center",
|
||||
"marginRight": -6,
|
||||
"marginTop": -32,
|
||||
"width": 34,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
name="expand"
|
||||
size={12}
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
@@ -6,12 +6,9 @@ import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
ScrollView,
|
||||
Platform,
|
||||
View,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {CELL_WIDTH} from 'app/components/markdown/markdown_table_cell/markdown_table_cell';
|
||||
|
||||
@@ -21,7 +18,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
|
||||
const MAX_HEIGHT = 300;
|
||||
const MAX_PREVIEW_COLUMNS = 5;
|
||||
|
||||
export default class MarkdownTable extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -41,23 +37,9 @@ export default class MarkdownTable extends React.PureComponent {
|
||||
containerWidth: 0,
|
||||
contentHeight: 0,
|
||||
contentWidth: 0,
|
||||
maxPreviewColumns: MAX_PREVIEW_COLUMNS,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Dimensions.addEventListener('change', this.setMaxPreviewColumns);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Dimensions.removeEventListener('change', this.setMaxPreviewColumns);
|
||||
}
|
||||
|
||||
setMaxPreviewColumns = ({window}) => {
|
||||
const maxPreviewColumns = Math.floor(window.width / CELL_WIDTH);
|
||||
this.setState({maxPreviewColumns});
|
||||
}
|
||||
|
||||
getTableWidth = () => {
|
||||
return this.props.numColumns * CELL_WIDTH;
|
||||
};
|
||||
@@ -90,38 +72,6 @@ export default class MarkdownTable extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
renderPreviewRows = (drawExtraBorders = true) => {
|
||||
const {maxPreviewColumns} = this.state;
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
const tableStyle = [style.table];
|
||||
if (drawExtraBorders) {
|
||||
tableStyle.push(style.tableExtraBorders);
|
||||
}
|
||||
|
||||
// Add an extra prop to the last row of the table so that it knows not to render a bottom border
|
||||
// since the container should be rendering that
|
||||
const rows = React.Children.toArray(this.props.children).slice(0, maxPreviewColumns).map((row) => {
|
||||
const children = React.Children.toArray(row.props.children).slice(0, maxPreviewColumns);
|
||||
return {
|
||||
...row,
|
||||
props: {
|
||||
...row.props,
|
||||
children,
|
||||
},
|
||||
};
|
||||
});
|
||||
rows[rows.length - 1] = React.cloneElement(rows[rows.length - 1], {
|
||||
isLastRow: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={tableStyle}>
|
||||
{rows}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderRows = (drawExtraBorders = true) => {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
@@ -157,7 +107,7 @@ export default class MarkdownTable extends React.PureComponent {
|
||||
]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 1, y: 0}}
|
||||
style={[style.moreRight, {height: this.state.contentHeight}]}
|
||||
style={style.moreRight}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -170,30 +120,13 @@ export default class MarkdownTable extends React.PureComponent {
|
||||
changeOpacity(this.props.theme.centerChannelColor, 0.0),
|
||||
changeOpacity(this.props.theme.centerChannelColor, 0.1),
|
||||
]}
|
||||
style={[style.moreBelow, {width: this.getTableWidth()}]}
|
||||
style={style.moreBelow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const expandButton = (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handlePress}
|
||||
style={{...style.expandButton, left: this.state.containerWidth - 20}}
|
||||
>
|
||||
<View style={[style.iconContainer, {width: this.getTableWidth()}]}>
|
||||
<View style={style.iconButton}>
|
||||
<Icon
|
||||
name={'expand'}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={style.tablePadding}
|
||||
onPress={this.handlePress}
|
||||
type={'opacity'}
|
||||
>
|
||||
@@ -205,11 +138,10 @@ export default class MarkdownTable extends React.PureComponent {
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[style.container, {maxWidth: this.getTableWidth()}]}
|
||||
>
|
||||
{this.renderPreviewRows(false)}
|
||||
{this.renderRows(false)}
|
||||
</ScrollView>
|
||||
{moreRight}
|
||||
{moreBelow}
|
||||
{expandButton}
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
@@ -223,66 +155,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderRightWidth: 1,
|
||||
maxHeight: MAX_HEIGHT,
|
||||
},
|
||||
expandButton: {
|
||||
height: 34,
|
||||
width: 34,
|
||||
},
|
||||
iconContainer: {
|
||||
maxWidth: '100%',
|
||||
alignItems: 'flex-end',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 4,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
paddingRight: 14,
|
||||
},
|
||||
}),
|
||||
},
|
||||
iconButton: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginTop: -32,
|
||||
marginRight: -6,
|
||||
borderWidth: 1,
|
||||
borderRadius: 50,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
width: 34,
|
||||
height: 34,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 14,
|
||||
color: theme.linkColor,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
fontSize: 13,
|
||||
},
|
||||
}),
|
||||
},
|
||||
table: {
|
||||
width: '100%',
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderLeftWidth: 1,
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
tablePadding: {
|
||||
paddingRight: 10,
|
||||
},
|
||||
tableExtraBorders: {
|
||||
borderBottomWidth: 1,
|
||||
borderRightWidth: 1,
|
||||
},
|
||||
moreBelow: {
|
||||
bottom: 30,
|
||||
bottom: 0,
|
||||
height: 20,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
},
|
||||
moreRight: {
|
||||
maxHeight: MAX_HEIGHT,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 20,
|
||||
},
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import MarkdownTable from './markdown_table';
|
||||
|
||||
describe('MarkdownTable', () => {
|
||||
const createCell = (type, children = null) => {
|
||||
return React.createElement('', {key: Date.now(), className: type}, children);
|
||||
};
|
||||
|
||||
const numColumns = 10;
|
||||
const children = [];
|
||||
for (let i = 0; i <= numColumns; i++) {
|
||||
const cols = [];
|
||||
for (let j = 0; j <= numColumns; j++) {
|
||||
cols.push(createCell('col'));
|
||||
}
|
||||
|
||||
children.push(createCell('row', cols));
|
||||
}
|
||||
|
||||
const baseProps = {
|
||||
children,
|
||||
numColumns,
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<MarkdownTable {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should slice rows and columns', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<MarkdownTable {...baseProps}/>
|
||||
);
|
||||
|
||||
const {maxPreviewColumns} = wrapper.state();
|
||||
expect(wrapper.find('.row')).toHaveLength(maxPreviewColumns);
|
||||
expect(wrapper.find('.col')).toHaveLength(Math.pow(maxPreviewColumns, 2));
|
||||
|
||||
const newMaxPreviewColumns = maxPreviewColumns - 1;
|
||||
wrapper.setState({maxPreviewColumns: newMaxPreviewColumns});
|
||||
expect(wrapper.find('.row')).toHaveLength(newMaxPreviewColumns);
|
||||
expect(wrapper.find('.col')).toHaveLength(Math.pow(newMaxPreviewColumns, 2));
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import ActionButtonText from './action_button_text';
|
||||
|
||||
export default class ActionButton extends PureComponent {
|
||||
@@ -19,7 +19,6 @@ export default class ActionButton extends PureComponent {
|
||||
postId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
cookie: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleActionPress = preventDoubleTap(() => {
|
||||
@@ -28,15 +27,13 @@ export default class ActionButton extends PureComponent {
|
||||
}, 4000);
|
||||
|
||||
render() {
|
||||
const {name, theme, disabled} = this.props;
|
||||
const {name, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<Button
|
||||
containerStyle={style.button}
|
||||
disabledContainerStyle={style.buttonDisabled}
|
||||
onPress={this.handleActionPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ActionButtonText
|
||||
message={name}
|
||||
@@ -52,7 +49,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
button: {
|
||||
borderRadius: 2,
|
||||
backgroundColor: theme.buttonBg,
|
||||
opacity: 1,
|
||||
alignItems: 'center',
|
||||
marginBottom: 2,
|
||||
marginRight: 5,
|
||||
@@ -60,9 +56,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: changeOpacity(theme.buttonBg, 0.3),
|
||||
},
|
||||
text: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 12,
|
||||
|
||||
@@ -18,7 +18,6 @@ export default class ActionMenu extends PureComponent {
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
postId: PropTypes.string.isRequired,
|
||||
selected: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -63,7 +62,6 @@ export default class ActionMenu extends PureComponent {
|
||||
name,
|
||||
dataSource,
|
||||
options,
|
||||
disabled,
|
||||
} = this.props;
|
||||
const {selected} = this.state;
|
||||
|
||||
@@ -74,7 +72,6 @@ export default class ActionMenu extends PureComponent {
|
||||
options={options}
|
||||
selected={selected}
|
||||
onSelected={this.handleSelect}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,14 +52,4 @@ describe('ActionMenu', () => {
|
||||
|
||||
expect(wrapper.state('selected')).toBe(props.selected);
|
||||
});
|
||||
|
||||
test('disabled works', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
disabled: true,
|
||||
};
|
||||
const wrapper = shallow(<ActionMenu {...props}/>);
|
||||
|
||||
expect(wrapper.props().disabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ export default class AttachmentActions extends PureComponent {
|
||||
defaultOption={action.default_option}
|
||||
options={action.options}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -54,7 +53,6 @@ export default class AttachmentActions extends PureComponent {
|
||||
cookie={action.cookie}
|
||||
name={action.name}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -23,6 +23,7 @@ exports[`AttachmentImage it matches snapshot 1`] = `
|
||||
"borderRadius": 2,
|
||||
"borderWidth": 1,
|
||||
"flex": 1,
|
||||
"padding": 5,
|
||||
},
|
||||
Object {
|
||||
"height": 28,
|
||||
@@ -32,14 +33,6 @@ exports[`AttachmentImage it matches snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<Connect(ProgressiveImage)
|
||||
imageStyle={
|
||||
Object {
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 2.5,
|
||||
"marginRight": 5,
|
||||
"marginTop": 2.5,
|
||||
}
|
||||
}
|
||||
imageUri="https://images.com/image.png"
|
||||
resizeMode="contain"
|
||||
style={
|
||||
|
||||
@@ -52,14 +52,6 @@ export default class AttachmentImage extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
setImageRef = (ref) => {
|
||||
this.imageRef = ref;
|
||||
}
|
||||
|
||||
setItemRef = (ref) => {
|
||||
this.itemRef = ref;
|
||||
}
|
||||
|
||||
handlePreviewImage = () => {
|
||||
const {imageUrl} = this.props;
|
||||
const {
|
||||
@@ -87,7 +79,7 @@ export default class AttachmentImage extends PureComponent {
|
||||
localPath: uri,
|
||||
},
|
||||
}];
|
||||
previewImageAtIndex([this.itemRef], 0, files);
|
||||
previewImageAtIndex([this.refs.item], 0, files);
|
||||
};
|
||||
|
||||
setImageDimensions = (imageUri, dimensions, originalWidth, originalHeight) => {
|
||||
@@ -140,8 +132,7 @@ export default class AttachmentImage extends PureComponent {
|
||||
if (imageUri) {
|
||||
progressiveImage = (
|
||||
<ProgressiveImage
|
||||
ref={this.setImageRef}
|
||||
imageStyle={style.attachmentMargin}
|
||||
ref='image'
|
||||
style={{height, width}}
|
||||
imageUri={imageUri}
|
||||
resizeMode='contain'
|
||||
@@ -158,7 +149,7 @@ export default class AttachmentImage extends PureComponent {
|
||||
type={'none'}
|
||||
>
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
ref='item'
|
||||
style={[style.imageContainer, {width, height}]}
|
||||
>
|
||||
{progressiveImage}
|
||||
@@ -178,12 +169,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
borderWidth: 1,
|
||||
borderRadius: 2,
|
||||
flex: 1,
|
||||
},
|
||||
attachmentMargin: {
|
||||
marginTop: 2.5,
|
||||
marginLeft: 2.5,
|
||||
marginBottom: 5,
|
||||
marginRight: 5,
|
||||
padding: 5,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {Image, StyleSheet, View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
export default class AttachmentThumbnail extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -20,7 +19,7 @@ export default class AttachmentThumbnail extends PureComponent {
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<FastImage
|
||||
<Image
|
||||
source={{uri}}
|
||||
resizeMode='contain'
|
||||
resizeMethod='scale'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/a
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {connection} from 'app/actions/device';
|
||||
import {markChannelViewedAndReadOnReconnect, setChannelRetryFailed} from 'app/actions/views/channel';
|
||||
import {markChannelViewedAndRead, setChannelRetryFailed} from 'app/actions/views/channel';
|
||||
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
|
||||
import {getConnection, isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -35,7 +35,7 @@ function mapDispatchToProps(dispatch) {
|
||||
connection,
|
||||
initWebSocket,
|
||||
logout,
|
||||
markChannelViewedAndReadOnReconnect,
|
||||
markChannelViewedAndRead,
|
||||
setChannelRetryFailed,
|
||||
setCurrentUserStatusOffline,
|
||||
startPeriodicStatusUpdates,
|
||||
|
||||
@@ -34,7 +34,7 @@ const {
|
||||
ANDROID_TOP_PORTRAIT,
|
||||
IOS_TOP_LANDSCAPE,
|
||||
IOS_TOP_PORTRAIT,
|
||||
IOS_INSETS_TOP_PORTRAIT,
|
||||
IOSX_TOP_PORTRAIT,
|
||||
} = ViewTypes;
|
||||
|
||||
export default class NetworkIndicator extends PureComponent {
|
||||
@@ -43,7 +43,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
closeWebSocket: PropTypes.func.isRequired,
|
||||
connection: PropTypes.func.isRequired,
|
||||
initWebSocket: PropTypes.func.isRequired,
|
||||
markChannelViewedAndReadOnReconnect: PropTypes.func.isRequired,
|
||||
markChannelViewedAndRead: PropTypes.func.isRequired,
|
||||
logout: PropTypes.func.isRequired,
|
||||
setChannelRetryFailed: PropTypes.func.isRequired,
|
||||
setCurrentUserStatusOffline: PropTypes.func.isRequired,
|
||||
@@ -206,7 +206,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
if (iPhoneWithInsets && isLandscape) {
|
||||
return IOS_TOP_LANDSCAPE;
|
||||
} else if (iPhoneWithInsets) {
|
||||
return IOS_INSETS_TOP_PORTRAIT;
|
||||
return IOSX_TOP_PORTRAIT;
|
||||
} else if (isLandscape && !DeviceTypes.IS_TABLET) {
|
||||
return IOS_TOP_LANDSCAPE;
|
||||
}
|
||||
@@ -245,7 +245,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
// foreground by tapping a notification from another channel
|
||||
this.clearNotificationTimeout = setTimeout(() => {
|
||||
PushNotifications.clearChannelNotifications(currentChannelId);
|
||||
actions.markChannelViewedAndReadOnReconnect(currentChannelId);
|
||||
actions.markChannelViewedAndRead(currentChannelId);
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
TextInput,
|
||||
Text,
|
||||
TouchableWithoutFeedback,
|
||||
UIManager,
|
||||
requireNativeComponent,
|
||||
} from 'react-native';
|
||||
import {TextInput, Text, TouchableWithoutFeedback} from 'react-native';
|
||||
import UIManager from 'UIManager';
|
||||
import invariant from 'invariant';
|
||||
import requireNativeComponent from 'requireNativeComponent';
|
||||
|
||||
const AndroidTextInput = requireNativeComponent('PasteableTextInputAndroid');
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {isChannelReadOnlyById} from 'mattermost-redux/selectors/entities/channel
|
||||
import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getUser, getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isStartOfNewMessages} from 'mattermost-redux/utils/post_list';
|
||||
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
|
||||
@@ -41,9 +40,7 @@ function makeMapStateToProps() {
|
||||
const isPostCommentMention = makeIsPostCommentMention();
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const post = ownProps.post || getPost(state, ownProps.postId);
|
||||
const previousPostId = isStartOfNewMessages(ownProps.previousPostId) ? ownProps.beforePrevPostId : ownProps.previousPostId;
|
||||
const previousPost = getPost(state, previousPostId);
|
||||
const beforePrevPost = getPost(state, ownProps.beforePrevPostId);
|
||||
const previousPost = getPost(state, ownProps.previousPostId);
|
||||
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
@@ -54,7 +51,7 @@ function makeMapStateToProps() {
|
||||
let commentedOnPost = null;
|
||||
|
||||
if (ownProps.renderReplies && post && post.root_id) {
|
||||
if (previousPostId) {
|
||||
if (ownProps.previousPostId) {
|
||||
if (previousPost && (previousPost.id === post.root_id || previousPost.root_id === post.root_id)) {
|
||||
// Previous post is root post or previous post is in same thread
|
||||
isFirstReply = false;
|
||||
@@ -87,8 +84,6 @@ function makeMapStateToProps() {
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
isCommentMention,
|
||||
isLandscape: isLandscape(state),
|
||||
previousPostExists: Boolean(previousPost),
|
||||
beforePrevPostUserId: (beforePrevPost ? beforePrevPost.user_id : null),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,8 +68,6 @@ export default class Post extends PureComponent {
|
||||
location: PropTypes.string,
|
||||
isBot: PropTypes.bool,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
previousPostExists: PropTypes.bool,
|
||||
beforePrevPostUserId: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -252,8 +250,6 @@ export default class Post extends PureComponent {
|
||||
skipPinnedHeader,
|
||||
location,
|
||||
isLandscape,
|
||||
previousPostExists,
|
||||
beforePrevPostUserId,
|
||||
} = this.props;
|
||||
|
||||
if (!post) {
|
||||
@@ -303,8 +299,6 @@ export default class Post extends PureComponent {
|
||||
onUsernamePress={onUsernamePress}
|
||||
renderReplies={renderReplies}
|
||||
theme={theme}
|
||||
previousPostExists={previousPostExists}
|
||||
beforePrevPostUserId={beforePrevPostUserId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,9 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchData(this.props.link, this.props.openGraphData);
|
||||
}
|
||||
|
||||
@@ -64,10 +66,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
setItemRef = (ref) => {
|
||||
this.itemRef = ref;
|
||||
}
|
||||
|
||||
fetchData = (url, openGraphData) => {
|
||||
if (!openGraphData) {
|
||||
this.props.actions.getOpenGraphMetadata(url);
|
||||
@@ -209,7 +207,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
},
|
||||
}];
|
||||
|
||||
previewImageAtIndex([this.itemRef], 0, files);
|
||||
previewImageAtIndex([this.refs.item], 0, files);
|
||||
};
|
||||
|
||||
renderDescription = () => {
|
||||
@@ -251,7 +249,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={this.setItemRef}
|
||||
ref='item'
|
||||
style={[style.imageContainer, {width, height}]}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renderSystemMessage uses renderer for Channel Display Name update 1`] = `
|
||||
<Connect(Markdown)
|
||||
baseTextStyle={Object {}}
|
||||
onPostPress={[MockFunction]}
|
||||
textStyles={Object {}}
|
||||
value="{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renderSystemMessage uses renderer for Channel Header update 1`] = `
|
||||
<Connect(Markdown)
|
||||
baseTextStyle={Object {}}
|
||||
onPostPress={[MockFunction]}
|
||||
textStyles={Object {}}
|
||||
value="{username} updated the channel header from: {oldHeader} to: {newHeader}"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renderSystemMessage uses renderer for Channel Purpose update 1`] = `
|
||||
<Connect(Markdown)
|
||||
baseTextStyle={Object {}}
|
||||
onPostPress={[MockFunction]}
|
||||
textStyles={Object {}}
|
||||
value="{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renderSystemMessage uses renderer for archived channel 1`] = `
|
||||
<Connect(Markdown)
|
||||
baseTextStyle={Object {}}
|
||||
onPostPress={[MockFunction]}
|
||||
textStyles={Object {}}
|
||||
value="{username} archived the channel"
|
||||
/>
|
||||
`;
|
||||
@@ -13,7 +13,6 @@ import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
|
||||
import CombinedSystemMessage from 'app/components/combined_system_message';
|
||||
import {renderSystemMessage} from './system_message_helpers';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import MarkdownEmoji from 'app/components/markdown/markdown_emoji';
|
||||
@@ -247,7 +246,6 @@ export default class PostBody extends PureComponent {
|
||||
isFailed={isFailed}
|
||||
onLongPress={this.showPostOptions}
|
||||
postId={post.id}
|
||||
isReplyPost={this.props.isReplyPost}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -350,13 +348,6 @@ export default class PostBody extends PureComponent {
|
||||
const messageStyle = isSystemMessage ? [style.message, style.systemMessage] : style.message;
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
const messageStyles = {messageStyle, textStyles};
|
||||
const intl = this.context.intl;
|
||||
const systemMessage = renderSystemMessage(this.props, messageStyles, intl);
|
||||
if (systemMessage) {
|
||||
return systemMessage;
|
||||
}
|
||||
|
||||
let body;
|
||||
let messageComponent;
|
||||
if (hasBeenDeleted) {
|
||||
|
||||
@@ -9,9 +9,6 @@ import PostBodyAdditionalContent from 'app/components/post_body_additional_conte
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import PostBody from './post_body.js';
|
||||
import * as SystemMessageHelpers from './system_message_helpers';
|
||||
|
||||
jest.mock('./system_message_helpers');
|
||||
|
||||
describe('PostBody', () => {
|
||||
const baseProps = {
|
||||
@@ -125,9 +122,4 @@ describe('PostBody', () => {
|
||||
instance.measurePost(event);
|
||||
expect(wrapper.state('isLongPost')).toEqual(false);
|
||||
});
|
||||
|
||||
test('should return system message as post body', () => {
|
||||
shallowWithIntl(<PostBody {...baseProps}/>);
|
||||
expect(SystemMessageHelpers.renderSystemMessage.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
const renderUsername = (value) => {
|
||||
return (value[0] === '@') ? value : `@${value}`;
|
||||
};
|
||||
|
||||
const renderMessage = (postBodyProps, styles, intl, localeHolder, values) => {
|
||||
const {onPress} = postBodyProps;
|
||||
const {messageStyle, textStyles} = styles;
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
baseTextStyle={messageStyle}
|
||||
onPostPress={onPress}
|
||||
textStyles={textStyles}
|
||||
value={intl.formatMessage(localeHolder, values)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeaderChangeMessage = (postBodyProps, styles, intl) => {
|
||||
const {postProps} = postBodyProps;
|
||||
let values;
|
||||
|
||||
if (!postProps.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = renderUsername(postProps.username);
|
||||
const oldHeader = postProps.old_header;
|
||||
const newHeader = postProps.new_header;
|
||||
let localeHolder;
|
||||
|
||||
if (postProps.new_header) {
|
||||
if (postProps.old_header) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_from'),
|
||||
defaultMessage: '{username} updated the channel header from: {oldHeader} to: {newHeader}',
|
||||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
}
|
||||
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_header_message_and_forget.updated_to'),
|
||||
defaultMessage: '{username} updated the channel header to: {newHeader}',
|
||||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
} else if (postProps.old_header) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_header_message_and_forget.removed'),
|
||||
defaultMessage: '{username} removed the channel header (was: {oldHeader})',
|
||||
};
|
||||
|
||||
values = {username, oldHeader, newHeader};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderPurposeChangeMessage = (postBodyProps, styles, intl) => {
|
||||
const {postProps} = postBodyProps;
|
||||
let values;
|
||||
|
||||
if (!postProps.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = renderUsername(postProps.username);
|
||||
const oldPurpose = postProps.old_purpose;
|
||||
const newPurpose = postProps.new_purpose;
|
||||
let localeHolder;
|
||||
|
||||
if (postProps.new_purpose) {
|
||||
if (postProps.old_purpose) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_purpose_message.updated_from'),
|
||||
defaultMessage: '{username} updated the channel purpose from: {oldPurpose} to: {newPurpose}',
|
||||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
}
|
||||
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_purpose_message.updated_to'),
|
||||
defaultMessage: '{username} updated the channel purpose to: {newPurpose}',
|
||||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
} else if (postProps.old_purpose) {
|
||||
localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_purpose_message.removed'),
|
||||
defaultMessage: '{username} removed the channel purpose (was: {oldPurpose})',
|
||||
};
|
||||
|
||||
values = {username, oldPurpose, newPurpose};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderDisplayNameChangeMessage = (postBodyProps, styles, intl) => {
|
||||
const {postProps} = postBodyProps;
|
||||
const oldDisplayName = postProps.old_displayname;
|
||||
const newDisplayName = postProps.new_displayname;
|
||||
|
||||
if (!(postProps.username && postProps.old_displayname && postProps.new_displayname)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = renderUsername(postProps.username);
|
||||
const localeHolder = {
|
||||
id: t('mobile.system_message.update_channel_displayname_message_and_forget.updated_from'),
|
||||
defaultMessage: '{username} updated the channel display name from: {oldDisplayName} to: {newDisplayName}',
|
||||
};
|
||||
|
||||
const values = {username, oldDisplayName, newDisplayName};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
};
|
||||
|
||||
const renderArchivedMessage = (postBodyProps, styles, intl) => {
|
||||
const {postProps} = postBodyProps;
|
||||
|
||||
const username = renderUsername(postProps.username);
|
||||
const localeHolder = {
|
||||
id: t('mobile.system_message.channel_archived_message'),
|
||||
defaultMessage: '{username} archived the channel',
|
||||
};
|
||||
|
||||
const values = {username};
|
||||
return renderMessage(postBodyProps, styles, intl, localeHolder, values);
|
||||
};
|
||||
|
||||
const systemMessageRenderers = {
|
||||
[Posts.POST_TYPES.HEADER_CHANGE]: renderHeaderChangeMessage,
|
||||
[Posts.POST_TYPES.DISPLAYNAME_CHANGE]: renderDisplayNameChangeMessage,
|
||||
[Posts.POST_TYPES.PURPOSE_CHANGE]: renderPurposeChangeMessage,
|
||||
[Posts.POST_TYPES.CHANNEL_DELETED]: renderArchivedMessage,
|
||||
};
|
||||
|
||||
export const renderSystemMessage = (postBodyProps, styles, intl) => {
|
||||
const renderer = systemMessageRenderers[postBodyProps.postType];
|
||||
if (!renderer) {
|
||||
return null;
|
||||
}
|
||||
return renderer(postBodyProps, styles, intl);
|
||||
};
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as SystemMessageHelpers from './system_message_helpers';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
|
||||
const basePostBodyProps = {
|
||||
postProps: {
|
||||
username: 'username',
|
||||
},
|
||||
onPress: jest.fn(),
|
||||
};
|
||||
|
||||
const mockStyles = {
|
||||
messageStyle: {},
|
||||
textStyles: {},
|
||||
};
|
||||
|
||||
const mockIntl = {
|
||||
formatMessage: ({defaultMessage}) => defaultMessage,
|
||||
};
|
||||
|
||||
describe('renderSystemMessage', () => {
|
||||
test('uses renderer for Channel Header update', () => {
|
||||
const postBodyProps = {
|
||||
...basePostBodyProps,
|
||||
postProps: {
|
||||
...basePostBodyProps.postProps,
|
||||
old_header: 'old header',
|
||||
new_header: 'new header',
|
||||
},
|
||||
postType: Posts.POST_TYPES.HEADER_CHANGE,
|
||||
};
|
||||
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
|
||||
expect(renderedMessage).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('uses renderer for Channel Display Name update', () => {
|
||||
const postBodyProps = {
|
||||
...basePostBodyProps,
|
||||
postProps: {
|
||||
...basePostBodyProps.postProps,
|
||||
old_displayname: 'old displayname',
|
||||
new_displayname: 'new displayname',
|
||||
},
|
||||
postType: Posts.POST_TYPES.DISPLAYNAME_CHANGE,
|
||||
};
|
||||
|
||||
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
|
||||
expect(renderedMessage).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('uses renderer for Channel Purpose update', () => {
|
||||
const postBodyProps = {
|
||||
...basePostBodyProps,
|
||||
postProps: {
|
||||
...basePostBodyProps.postProps,
|
||||
old_purpose: 'old purpose',
|
||||
new_purpose: 'new purpose',
|
||||
},
|
||||
postType: Posts.POST_TYPES.PURPOSE_CHANGE,
|
||||
};
|
||||
|
||||
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
|
||||
expect(renderedMessage).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('uses renderer for archived channel', () => {
|
||||
const postBodyProps = {
|
||||
...basePostBodyProps,
|
||||
postProps: {
|
||||
...basePostBodyProps.postProps,
|
||||
},
|
||||
postType: Posts.POST_TYPES.CHANNEL_DELETED,
|
||||
};
|
||||
|
||||
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
|
||||
expect(renderedMessage).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('is null for non-qualifying system messages', () => {
|
||||
const postBodyProps = {
|
||||
...basePostBodyProps,
|
||||
postType: 'not_relevant',
|
||||
};
|
||||
|
||||
const renderedMessage = SystemMessageHelpers.renderSystemMessage(postBodyProps, mockStyles, mockIntl);
|
||||
expect(renderedMessage).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -87,22 +87,21 @@ export default class PostBodyAdditionalContent extends PureComponent {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
componentWillMount() {
|
||||
this.mounted = true;
|
||||
|
||||
this.load(this.props);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.link !== this.props.link) {
|
||||
this.load(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.link !== nextProps.link) {
|
||||
this.load(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
isImage = (specificLink) => {
|
||||
const {metadata, link} = this.props;
|
||||
|
||||
|
||||
@@ -1,683 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PostHeader should match snapshot when just a base post 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post is autoresponder 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</View>
|
||||
<Tag
|
||||
defaultMessage="AUTOMATIC REPLY"
|
||||
id="post_info.auto_responder"
|
||||
show={true}
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 0,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post is from system message 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="System"
|
||||
id="post_info.system"
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post is same thread, so dont display Commented On 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FormattedText
|
||||
defaultMessage="Commented on {name}{apostrophe} message: "
|
||||
id="post_body.commentedOn"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.65)",
|
||||
"lineHeight": 21,
|
||||
"marginBottom": 3,
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"apostrophe": "'s",
|
||||
"name": "John Doe",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyButton 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
Object {
|
||||
"maxWidth": "50%",
|
||||
},
|
||||
]
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<BotTag
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 5,
|
||||
"marginLeft": 0,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "flex-end",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "flex-end",
|
||||
"minWidth": 40,
|
||||
"paddingBottom": 10,
|
||||
"paddingTop": 2,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<ReplyIcon
|
||||
color="#2389d7"
|
||||
height={16}
|
||||
width={16}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 12,
|
||||
"marginLeft": 2,
|
||||
"marginTop": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
0
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post renders Commented On for new post 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FormattedText
|
||||
defaultMessage="Commented on {name}{apostrophe} message: "
|
||||
id="post_body.commentedOn"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.65)",
|
||||
"lineHeight": 21,
|
||||
"marginBottom": 3,
|
||||
}
|
||||
}
|
||||
values={
|
||||
Object {
|
||||
"apostrophe": "'s",
|
||||
"name": "John Doe",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`PostHeader should match snapshot when post should display reply button 1`] = `
|
||||
<React.Fragment>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"marginTop": 10,
|
||||
},
|
||||
false,
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
"maxWidth": "60%",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flexGrow": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"paddingVertical": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
"opacity": 0.5,
|
||||
}
|
||||
}
|
||||
timeZone=""
|
||||
value={0}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
"justifyContent": "flex-end",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "flex-start",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "flex-end",
|
||||
"minWidth": 40,
|
||||
"paddingBottom": 10,
|
||||
"paddingTop": 2,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
>
|
||||
<ReplyIcon
|
||||
color="#2389d7"
|
||||
height={16}
|
||||
width={16}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
"fontSize": 12,
|
||||
"marginLeft": 2,
|
||||
"marginTop": 2,
|
||||
}
|
||||
}
|
||||
>
|
||||
0
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -43,9 +43,6 @@ export default class PostHeader extends PureComponent {
|
||||
isGuest: PropTypes.bool,
|
||||
userTimezone: PropTypes.string,
|
||||
enableTimezone: PropTypes.bool,
|
||||
previousPostExists: PropTypes.bool,
|
||||
post: PropTypes.object,
|
||||
beforePrevPostUserId: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -61,18 +58,11 @@ export default class PostHeader extends PureComponent {
|
||||
};
|
||||
|
||||
renderCommentedOnMessage = () => {
|
||||
const {
|
||||
beforePrevPostUserId,
|
||||
commentedOnDisplayName,
|
||||
post,
|
||||
previousPostExists,
|
||||
renderReplies,
|
||||
theme,
|
||||
} = this.props;
|
||||
if (!renderReplies || !commentedOnDisplayName || (!previousPostExists && post.user_id === beforePrevPostUserId)) {
|
||||
if (!this.props.renderReplies || !this.props.commentedOnDisplayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {commentedOnDisplayName, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const displayName = commentedOnDisplayName;
|
||||
|
||||
@@ -117,16 +107,9 @@ export default class PostHeader extends PureComponent {
|
||||
fromAutoResponder,
|
||||
overrideUsername,
|
||||
theme,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
commentedOnDisplayName,
|
||||
commentCount,
|
||||
isBot,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const showReply = shouldRenderReplyButton || (!commentedOnDisplayName && commentCount > 0 && renderReplies);
|
||||
const displayNameStyle = [style.displayNameContainer, showReply && (isBot || fromAutoResponder || fromWebHook) ? style.displayNameContainerBotReplyWidth : null];
|
||||
|
||||
if (fromAutoResponder || fromWebHook) {
|
||||
let name = displayName;
|
||||
@@ -135,7 +118,7 @@ export default class PostHeader extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={displayNameStyle}>
|
||||
<View style={[style.displayNameContainer, {maxWidth: fromAutoResponder ? '30%' : '60%'}]}>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
ellipsizeMode={'tail'}
|
||||
@@ -159,7 +142,7 @@ export default class PostHeader extends PureComponent {
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handleUsernamePress}
|
||||
style={displayNameStyle}
|
||||
style={style.displayNameContainer}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text
|
||||
@@ -364,8 +347,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
marginBottom: 3,
|
||||
lineHeight: 21,
|
||||
},
|
||||
displayNameContainerBotReplyWidth: {
|
||||
maxWidth: '50%',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import PostHeader from './post_header';
|
||||
|
||||
describe('PostHeader', () => {
|
||||
const baseProps = {
|
||||
commentCount: 0,
|
||||
commentedOnDisplayName: '',
|
||||
createAt: 0,
|
||||
displayName: 'John Smith',
|
||||
enablePostUsernameOverride: false,
|
||||
fromWebHook: false,
|
||||
isPendingOrFailedPost: false,
|
||||
isSearchResult: false,
|
||||
isSystemMessage: false,
|
||||
fromAutoResponder: false,
|
||||
militaryTime: false,
|
||||
overrideUsername: '',
|
||||
renderReplies: false,
|
||||
shouldRenderReplyButton: false,
|
||||
showFullDate: false,
|
||||
theme: Preferences.THEMES.default,
|
||||
username: 'JohnSmith',
|
||||
isBot: false,
|
||||
isGuest: false,
|
||||
userTimezone: '',
|
||||
enableTimezone: false,
|
||||
previousPostExists: false,
|
||||
post: {id: 'post'},
|
||||
beforePrevPostUserId: '0',
|
||||
onPress: jest.fn(),
|
||||
isFirstReply: true,
|
||||
};
|
||||
|
||||
test('should match snapshot when just a base post', () => {
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...baseProps}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should match snapshot when post isBot and shouldRenderReplyButton', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
shouldRenderReplyButton: true,
|
||||
isBot: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when post should display reply button', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
shouldRenderReplyButton: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when post is autoresponder', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
fromAutoResponder: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should match snapshot when post is from system message', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isSystemMessage: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('should match snapshot when post renders Commented On for new post', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isFirstReply: true,
|
||||
renderReplies: true,
|
||||
commentedOnDisplayName: 'John Doe',
|
||||
previousPostExists: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when post is same thread, so dont display Commented On', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isFirstReply: false,
|
||||
renderReplies: true,
|
||||
commentedOnDisplayName: 'John Doe',
|
||||
previousPostExists: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -41,18 +41,7 @@ exports[`PostList setting channel deep link 1`] = `
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollToIndexFailed={[Function]}
|
||||
refreshControl={
|
||||
<RefreshControlMock
|
||||
colors={
|
||||
Array [
|
||||
"#3d3c40",
|
||||
]
|
||||
}
|
||||
onRefresh={null}
|
||||
refreshing={false}
|
||||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
refreshing={false}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
@@ -102,18 +91,7 @@ exports[`PostList setting permalink deep link 1`] = `
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollToIndexFailed={[Function]}
|
||||
refreshControl={
|
||||
<RefreshControlMock
|
||||
colors={
|
||||
Array [
|
||||
"#3d3c40",
|
||||
]
|
||||
}
|
||||
onRefresh={null}
|
||||
refreshing={false}
|
||||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
refreshing={false}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
@@ -163,18 +141,7 @@ exports[`PostList should match snapshot 1`] = `
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
onScrollToIndexFailed={[Function]}
|
||||
refreshControl={
|
||||
<RefreshControlMock
|
||||
colors={
|
||||
Array [
|
||||
"#3d3c40",
|
||||
]
|
||||
}
|
||||
onRefresh={null}
|
||||
refreshing={false}
|
||||
tintColor="#3d3c40"
|
||||
/>
|
||||
}
|
||||
refreshing={false}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {FlatList, RefreshControl, StyleSheet} from 'react-native';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import * as PostListUtils from 'mattermost-redux/utils/post_list';
|
||||
@@ -260,7 +260,6 @@ export default class PostList extends PureComponent {
|
||||
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
|
||||
// comes after this one in the list
|
||||
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
|
||||
const beforePrevPostId = index < this.props.postIds.length - 2 ? this.props.postIds[index + 2] : null;
|
||||
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
|
||||
|
||||
const postProps = {
|
||||
@@ -275,7 +274,6 @@ export default class PostList extends PureComponent {
|
||||
onPress: this.props.onPostPress,
|
||||
renderReplies: this.props.renderReplies,
|
||||
shouldRenderReplyButton: this.props.shouldRenderReplyButton,
|
||||
beforePrevPostId,
|
||||
};
|
||||
|
||||
if (PostListUtils.isCombinedUserActivityPost(item)) {
|
||||
@@ -366,16 +364,13 @@ export default class PostList extends PureComponent {
|
||||
postIds,
|
||||
refreshing,
|
||||
scrollViewNativeID,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
const refreshControl = (
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={channelId ? this.handleRefresh : null}
|
||||
colors={[theme.centerChannelColor]}
|
||||
tintColor={theme.centerChannelColor}
|
||||
/>);
|
||||
const refreshControl = {refreshing};
|
||||
|
||||
if (channelId) {
|
||||
refreshControl.onRefresh = this.handleRefresh;
|
||||
}
|
||||
|
||||
const hasPostsKey = postIds.length ? 'true' : 'false';
|
||||
|
||||
@@ -401,7 +396,7 @@ export default class PostList extends PureComponent {
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
scrollEventThrottle={60}
|
||||
refreshControl={refreshControl}
|
||||
{...refreshControl}
|
||||
nativeID={scrollViewNativeID}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,6 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
@@ -123,7 +122,6 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user