Compare commits
16 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
768d843e37 | ||
|
|
6b8c36171c | ||
|
|
87cb6d68db | ||
|
|
71654ccfe9 | ||
|
|
3da18b4dae | ||
|
|
db09e1376a | ||
|
|
2d5dba1fc0 | ||
|
|
7c3f4b9659 | ||
|
|
666e99dd76 | ||
|
|
fbf0f8344f | ||
|
|
08743639bf | ||
|
|
a4bdda15ea | ||
|
|
27b3001fd3 | ||
|
|
78a80c4afe | ||
|
|
d55766f039 | ||
|
|
8eaf11d284 |
16
.babelrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"presets": [ "react-native" ],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": ["transform-remove-console"]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
["module-resolver", {
|
||||
"root": ["./src", "."],
|
||||
"alias": {
|
||||
"assets": "./dist/assets"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,7 @@ charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[{package.json,.eslintrc.json}]
|
||||
[webapp/package.json]
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
|
||||
287
.eslintrc.json
@@ -1,31 +1,262 @@
|
||||
{
|
||||
"extends": [
|
||||
"./node_modules/eslint-config-mattermost/.eslintrc.json",
|
||||
"./node_modules/eslint-config-mattermost/.eslintrc-react.json"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "16.5"
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"impliedStrict": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react",
|
||||
"mocha"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jquery": true,
|
||||
"es6": true
|
||||
},
|
||||
"globals": {
|
||||
"jest": true,
|
||||
"describe": true,
|
||||
"it": true,
|
||||
"expect": true,
|
||||
"before": true,
|
||||
"beforeEach": true,
|
||||
"after": true,
|
||||
"afterEach": true
|
||||
},
|
||||
"rules": {
|
||||
"array-bracket-spacing": [2, "never"],
|
||||
"array-callback-return": 2,
|
||||
"arrow-body-style": 0,
|
||||
"arrow-parens": [2, "always"],
|
||||
"arrow-spacing": [2, { "before": true, "after": true }],
|
||||
"block-scoped-var": 2,
|
||||
"brace-style": [2, "1tbs", { "allowSingleLine": false }],
|
||||
"camelcase": [2, {"properties": "never"}],
|
||||
"class-methods-use-this": 0,
|
||||
"comma-dangle": [2, "never"],
|
||||
"comma-spacing": [2, {"before": false, "after": true}],
|
||||
"comma-style": [2, "last"],
|
||||
"complexity": [1, 10],
|
||||
"computed-property-spacing": [2, "never"],
|
||||
"consistent-return": 2,
|
||||
"consistent-this": [2, "self"],
|
||||
"constructor-super": 2,
|
||||
"curly": [2, "all"],
|
||||
"dot-location": [2, "object"],
|
||||
"dot-notation": 2,
|
||||
"eqeqeq": [2, "smart"],
|
||||
"func-call-spacing": [2, "never"],
|
||||
"func-names": 2,
|
||||
"func-style": [2, "declaration"],
|
||||
"generator-star-spacing": [0, {"before": false, "after": true}],
|
||||
"global-require": 2,
|
||||
"guard-for-in": 2,
|
||||
"id-blacklist": 0,
|
||||
"indent": [2, 4, {"SwitchCase": 0}],
|
||||
"jsx-quotes": [2, "prefer-single"],
|
||||
"key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "strict"}],
|
||||
"keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}],
|
||||
"line-comment-position": 0,
|
||||
"linebreak-style": 2,
|
||||
"lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
|
||||
"max-lines": [1, {"max": 450, "skipBlankLines": true, "skipComments": false}],
|
||||
"max-nested-callbacks": [2, {"max":2}],
|
||||
"max-statements-per-line": [2, {"max": 1}],
|
||||
"multiline-ternary": [1, "never"],
|
||||
"new-cap": 2,
|
||||
"new-parens": 2,
|
||||
"newline-before-return": 0,
|
||||
"newline-per-chained-call": 0,
|
||||
"no-alert": 2,
|
||||
"no-array-constructor": 2,
|
||||
"no-caller": 2,
|
||||
"no-case-declarations": 2,
|
||||
"no-class-assign": 2,
|
||||
"no-cond-assign": [2, "except-parens"],
|
||||
"no-confusing-arrow": 2,
|
||||
"no-console": 2,
|
||||
"no-const-assign": 2,
|
||||
"no-constant-condition": 2,
|
||||
"no-debugger": 2,
|
||||
"no-div-regex": 2,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-class-members": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-duplicate-imports": [2, {"includeExports": true}],
|
||||
"no-else-return": 2,
|
||||
"no-empty": 2,
|
||||
"no-empty-function": 2,
|
||||
"no-empty-pattern": 2,
|
||||
"no-eval": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-label": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-extra-semi": 2,
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-global-assign": 2,
|
||||
"no-implicit-coercion": 2,
|
||||
"no-implicit-globals": 0,
|
||||
"no-implied-eval": 2,
|
||||
"no-inner-declarations": 0,
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-lonely-if": 2,
|
||||
"no-loop-func": 2,
|
||||
"no-magic-numbers": 0,
|
||||
"no-mixed-operators": [2, {"allowSamePrecedence": false}],
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-spaces": [2, { "exceptions": { "Property": false } }],
|
||||
"no-multi-str": 0,
|
||||
"no-multiple-empty-lines": [2, {"max": 1}],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-condition": 2,
|
||||
"no-nested-ternary": 2,
|
||||
"no-new": 2,
|
||||
"no-new-func": 2,
|
||||
"no-new-object": 2,
|
||||
"no-new-symbol": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-param-reassign": 2,
|
||||
"no-process-env": 2,
|
||||
"no-process-exit": 2,
|
||||
"no-proto": 2,
|
||||
"no-redeclare": 2,
|
||||
"no-return-assign": [2, "always"],
|
||||
"no-script-url": 2,
|
||||
"no-self-assign": [2, {"props": true}],
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": [2, {"hoist": "functions"}],
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-tabs": 0,
|
||||
"no-template-curly-in-string": 2,
|
||||
"no-ternary": 0,
|
||||
"no-this-before-super": 2,
|
||||
"no-throw-literal": 0,
|
||||
"no-trailing-spaces": [2, { "skipBlankLines": false }],
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 2,
|
||||
"no-underscore-dangle": 2,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unmodified-loop-condition": 2,
|
||||
"no-unneeded-ternary": [2, {"defaultAssignment": false}],
|
||||
"no-unreachable": 2,
|
||||
"no-unsafe-finally": 2,
|
||||
"no-unsafe-negation": 2,
|
||||
"no-unused-expressions": 2,
|
||||
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
|
||||
"no-use-before-define": [2, {"classes": false, "functions": false, "variables": false}],
|
||||
"no-useless-computed-key": 2,
|
||||
"no-useless-concat": 2,
|
||||
"no-useless-constructor": 2,
|
||||
"no-useless-escape": 2,
|
||||
"no-useless-rename": 2,
|
||||
"no-var": 0,
|
||||
"no-void": 2,
|
||||
"no-warning-comments": 1,
|
||||
"no-whitespace-before-property": 2,
|
||||
"no-with": 2,
|
||||
"object-curly-newline": 0,
|
||||
"object-curly-spacing": [2, "never"],
|
||||
"object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}],
|
||||
"object-shorthand": [2, "always"],
|
||||
"one-var": [2, "never"],
|
||||
"one-var-declaration-per-line": 0,
|
||||
"operator-linebreak": [2, "after"],
|
||||
"padded-blocks": [2, "never"],
|
||||
"prefer-arrow-callback": 2,
|
||||
"prefer-const": 2,
|
||||
"prefer-numeric-literals": 2,
|
||||
"prefer-reflect": 2,
|
||||
"prefer-rest-params": 2,
|
||||
"prefer-spread": 2,
|
||||
"prefer-template": 0,
|
||||
"quote-props": [2, "as-needed"],
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"radix": 2,
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-boolean-value": [2, "always"],
|
||||
"react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }],
|
||||
"react/jsx-curly-spacing": [2, "never"],
|
||||
"react/jsx-equals-spacing": [2, "never"],
|
||||
"react/jsx-filename-extension": [2, {"extensions": [".js"]}],
|
||||
"react/jsx-first-prop-new-line": [2, "multiline"],
|
||||
"react/jsx-handler-names": 0,
|
||||
"react/jsx-indent": [2, 4],
|
||||
"react/jsx-indent-props": [2, 4],
|
||||
"react/jsx-key": 2,
|
||||
"react/jsx-max-props-per-line": [2, { "maximum": 1 }],
|
||||
"react/jsx-no-bind": 0,
|
||||
"react/jsx-no-duplicate-props": [2, { "ignoreCase": false }],
|
||||
"react/jsx-no-literals": 2,
|
||||
"react/jsx-no-target-blank": 2,
|
||||
"react/jsx-no-undef": 2,
|
||||
"react/jsx-pascal-case": 2,
|
||||
"react/jsx-tag-spacing": [2, {"closingSlash": "never", "beforeSelfClosing": "never", "afterOpening": "never"}],
|
||||
"react/jsx-uses-react": 2,
|
||||
"react/jsx-uses-vars": 2,
|
||||
"react/jsx-no-comment-textnodes": 2,
|
||||
"react/no-danger": 0,
|
||||
"react/no-deprecated": 2,
|
||||
"react/no-did-mount-set-state": 2,
|
||||
"react/no-did-update-set-state": 2,
|
||||
"react/no-direct-mutation-state": 2,
|
||||
"react/no-is-mounted": 2,
|
||||
"react/no-multi-comp": [2, { "ignoreStateless": true }],
|
||||
"react/no-render-return-value": 2,
|
||||
"react/no-set-state": 0,
|
||||
"react/no-string-refs": 0,
|
||||
"react/no-unknown-property": 2,
|
||||
"react/prefer-es6-class": 2,
|
||||
"react/prefer-stateless-function": 0,
|
||||
"react/prop-types": 2,
|
||||
"react/require-optimization": 1,
|
||||
"react/require-render-return": 2,
|
||||
"react/self-closing-comp": 2,
|
||||
"react/sort-comp": 0,
|
||||
"react/jsx-wrap-multilines": 2,
|
||||
"react/no-find-dom-node": 1,
|
||||
"react/forbid-component-props": 0,
|
||||
"react/no-danger-with-children": 2,
|
||||
"react/no-unused-prop-types": [1, {"skipShapeProps": true}],
|
||||
"react/style-prop-object": 2,
|
||||
"react/no-children-prop": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"require-yield": 2,
|
||||
"rest-spread-spacing": [2, "never"],
|
||||
"semi": [2, "always"],
|
||||
"semi-spacing": [2, {"before": false, "after": true}],
|
||||
"sort-imports": 0,
|
||||
"sort-keys": 0,
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": [2, {"anonymous": "never", "named": "never", "asyncArrow": "always"}],
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
||||
"symbol-description": 2,
|
||||
"template-curly-spacing": [2, "never"],
|
||||
"valid-typeof": [2, {"requireStringLiterals": false}],
|
||||
"vars-on-top": 0,
|
||||
"wrap-iife": [2, "outside"],
|
||||
"wrap-regex": 2,
|
||||
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
|
||||
"mocha/no-exclusive-tests": 2
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": true
|
||||
},
|
||||
"rules": {
|
||||
"global-require": 0,
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-filename-extension": [2, {"extensions": [".js"]}]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.test.js", "*.test.jsx"],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
34
.flowconfig
@@ -16,55 +16,33 @@
|
||||
; Ignore polyfills
|
||||
.*/Libraries/polyfills/.*
|
||||
|
||||
; 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
|
||||
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
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='^[./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_type=$FixMe
|
||||
|
||||
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\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-3]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-3]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
^0.92.0
|
||||
^0.53.0
|
||||
|
||||
18
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
assets/override
|
||||
dist
|
||||
build-ios
|
||||
*.zip
|
||||
server.PID
|
||||
mattermost.keystore
|
||||
@@ -21,7 +20,6 @@ build/
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
xcshareddata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
@@ -30,10 +28,10 @@ DerivedData
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
xcshareddata/
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
@@ -69,11 +67,10 @@ tags
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
*/fastlane/report.xml
|
||||
*/fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
fastlane/.env
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
@@ -81,14 +78,7 @@ ios/sentry.properties
|
||||
|
||||
# Testing
|
||||
.nyc_output
|
||||
coverage
|
||||
|
||||
# Pods
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
|
||||
621
CHANGELOG.md
@@ -1,626 +1,5 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## 1.19.0 Release
|
||||
- Release Date: May 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Android managed config was lost on the thread view.
|
||||
- Fixed an issue where contents of ephemeral posts did not display on the mobile app.
|
||||
- Fixed a few mobile app crash / fatal error issues.
|
||||
- Fixed an issue with an expanding animation when tapping on Jump to Channel in the channel list.
|
||||
- Fixed an issue on iOS where animated custom emoji weren't animated.
|
||||
- Fixed an issue on iOS where users were unable to create channel name of 2 characters.
|
||||
- Fixed an issue on iOS where emoji appeared too close, with uneven spacing, and too small in the info modal.
|
||||
- Added an error handler when sharing text that was over server's maximum post size with the iOS Share Extension.
|
||||
- Fixed an issue where users could upload a GIF as a profile image.
|
||||
|
||||
### Known Issues
|
||||
- Buttons inside ephemeral posts are not clickable / functional on the mobile app.
|
||||
|
||||
## 1.18.1 Release
|
||||
- Release Date: April 18, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed a crash issue caused by a malformed post textbox localize string.
|
||||
- Fixed an issue where iOS crashed when trying to log in using SSO and the SSO provider set a cookie without an expiration date.
|
||||
|
||||
## 1.18.0 Release
|
||||
- Release Date: April 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
- ``Bot`` tags were added for bot accounts feature in server v5.10 and mobile v1.18, meaning that mobile v1.17 and earlier don't support the tags.
|
||||
|
||||
### Highlights
|
||||
- Added support for Office365 single sign-on (SSO).
|
||||
- Added support for Integrated Windows Authentication (IWA).
|
||||
|
||||
### Improvements
|
||||
- Added the ability for channel links to open inside the app.
|
||||
- Added ability for emojis and hyperlinks to render in the message attachment title.
|
||||
- Added Chinese support for words that trigger mentions.
|
||||
- Added a setting to the system console to change the minimum length of hashtags.
|
||||
- Added a reply option to long press context menu.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where blank spaces broke markdown tables.
|
||||
- Fixed an issue where deactivated users appeared on "Add Members" modal but not on the search results.
|
||||
- Fixed an issue on Android where extra text in the search box appeared after using the autocomplete drop-down.
|
||||
- Fixed an issue with multiple text entries when typing with Shift+Letter on Android.
|
||||
- Fixed an issue where push notifications badges did not always clear when read on another device.
|
||||
- Fixed an issue where opening a single or group notification did not take the user into the channel where the notification came from.
|
||||
- Fixed an issue where timezone did not automatically update on Android when travelling to another timezone.
|
||||
- Fixed an issue where the user mention autocomplete drop-down was case sensitive.
|
||||
- Fixed an issue where system admininistrators were able to see the full long press menu when long pressing a system message.
|
||||
- Fixed an issue where users were not able to unflag posts from "Flagged Posts" when opened from a read-only channel.
|
||||
- Fixed an issue where users were unable to create channel names of 2 byte characters.
|
||||
|
||||
### Known Issues
|
||||
- Content for ephemeral messages is not displayed on Mattermost Mobile Apps.
|
||||
|
||||
## 1.17.0 Release
|
||||
- Release Date: March 20, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
|
||||
- If you are using an EMM provider via AppConfig, make sure to add two new settings, `useVPN` and `timeoutVPN`, to your AppConfig file. The settings were added for EMM connections using VPN on-demand - one to indicate if every request should wait for the VPN connection to be established, and another to set the timeout in seconds. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
- iPhone 5s devices and later with iOS 11+ is required.
|
||||
|
||||
### Highlights
|
||||
- iOS Share Extension now supports large file sizes and improved performance
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed support for EMM connections using VPN on-demand. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
|
||||
- Fixed several Android app crash / fatal error issues.
|
||||
- Fixed an issue on Android where the app crashed intermittently when selecting a link.
|
||||
- Fixed an issue where email notifications setting was out of sync with the webapp until the setting was edited.
|
||||
- Fixed an issue where notification badges were not cleared from other clients when clicking on a push notification after opening the mobile app.
|
||||
- Fixed an issue where the app did not show local notification when session expired.
|
||||
- Fixed an issue where the profile picture for webhooks was showing the hook owner picture.
|
||||
- Fixed an issue where some emoji were not rendered as jumbo.
|
||||
- Fixed an issue where jumbo emoji posted as a reply sometimes appeared with large space beneath.
|
||||
- Fixed an issue where the "No Internet Connection" banner did not always display when internet connectivity was lost.
|
||||
- Fixed an issue where the "No Internet Connection" banner did not always disappear when connection was re-estabilished.
|
||||
- Fixed an issue where opening channels with unreads had loading indicator placed above unread messages line.
|
||||
|
||||
## 1.16.1 Release
|
||||
- Release Date: February 21, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where link previews and reactions weren't displayed when post metadata was disabled.
|
||||
- Fixed an issue on Android where the app crashed when sharing multiple files.
|
||||
|
||||
## 1.16.0 Release
|
||||
- Release Date: February 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
### Improvements
|
||||
- Added the ability to remove own profile picture.
|
||||
- Changed "X" to "Cancel" on Edit Profile page.
|
||||
- Added support for relative permalinks.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the iOS app did not wait until the on-demand VPN connection was established. (EMM Providers)
|
||||
- Fixed an issue with a white screen caused by missing Russian translations.
|
||||
- Fixed an issue where the iOS badge notification did not always clear.
|
||||
- Fixed an issue where the thread view displayed a new message indicator.
|
||||
- Fixed an issue where quick multiple taps on the file icon opened multiple file previews.
|
||||
- Fixed an issue where the settings page did not show an option to join other teams.
|
||||
- Fixed an issue where image previews didn't work after using Delete File Cache.
|
||||
- Fixed an issue on Android where the notification trigger word modal title was "Send email notifications" instead of "Keywords".
|
||||
- Fixed an issue where the Webhook icon was misaligned and bottom edges were cut off.
|
||||
- Fixed an issue on Android where the user was not asked to authenticate to the app first when trying to share a photo, resulting in a white "Share modal" screen with a never-ending loading indicator.
|
||||
- Fixed an issue on iOS where push notifications were not preserved when opening the app via the Mattermost icon.
|
||||
|
||||
## 1.15.2 Release
|
||||
- Release Date: January 16, 2019
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed an issue where the status changes for other users did not always stay current in the mobile app.
|
||||
- Fixed an issue where a post did not fail properly when the user attempted to send the post while there was no network access.
|
||||
- Fixed an issue where date separators did not update when changing timezones.
|
||||
- Fixed an issue where the Favorites section did not clear from a users's channel drawer.
|
||||
- Removed an extra divider below "Edit Channel" of Direct Message Channel Info.
|
||||
- Fixed an issue where a user was not returned to previously viewed channel after viewing and then closing an archived channel.
|
||||
- Fixed an issue where a quick double tap on switch of Channel Info created and extra on/off state.
|
||||
- Fixed an issue where iOS long press menu didn't have rounded corners.
|
||||
|
||||
## 1.15.1 Release
|
||||
- Release Date: December 28, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue preventing some users from logging in using OKTA.
|
||||
|
||||
## 1.15.0 Release
|
||||
- Release Date: December 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
### Combatibility
|
||||
|
||||
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
|
||||
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
|
||||
|
||||
### Highlights
|
||||
- Added mention and reply mention highlighting.
|
||||
- Added a sliding animation for the reaction list.
|
||||
- Added support for pinned posts.
|
||||
- Added support for jumbo emojis.
|
||||
- Added support for interactive dialogs.
|
||||
- Improved UI for the long press menu and emoji reaction viewer.
|
||||
|
||||
### Improvements
|
||||
- Added the ability to include custom headers with requests for custom builds.
|
||||
- Push Notifications that are grouped by channels are cleared once the channel is read.
|
||||
- Improved auto-reconnect when unable to reach the server.
|
||||
- Added support for changing the mobile client status to offline when the app loses connection.
|
||||
- Added 'View Members' button to archived channels.
|
||||
- Added support on iOS for keeping the postlist in place without scrolling when new content is available.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where clicking on a file did not show downloading progress.
|
||||
- Fixed an issue on Android where on fresh install the share extension would not properly show available channels.
|
||||
- Fixed an issue where recently archived channels remained in in: autocomplete when they had been archived.
|
||||
- Fixed an issue where text should render when no actual custom emoji matched the named emoji pattern.
|
||||
- Fixed an issue on iOS where text got cut-off after replying to a message.
|
||||
- Fixed an issue where search modifier for channels was showing Direct Messages without usernames.
|
||||
- Fixed an issue where "Close Channel" did not work properly when viewing two archived channels in a row.
|
||||
- Fixed an issue with "Critical Error" screen when trying to upload certain file types from "+" to the left of message input box.
|
||||
|
||||
## 1.14.0 Release
|
||||
- Release Date: November 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
|
||||
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the Android app did not allow establishing a network connection with any server that used a self-signed certificate that had the CA certificate user installed on the device.
|
||||
- Removed "Copy Post" option on long-press message menu for posts without text.
|
||||
- Fixed an issue where the "Search Results" header was not fully scrolled to top on search "from:username".
|
||||
- Fixed an issue where channel names truncated at fewer characters than necessary.
|
||||
- Fixed an issue where the same uploaded photo generated a different file size.
|
||||
- Fixed an issue where the "(you)" was not displayed to the right of a user's name in the channel drawer when a user opened a Direct Message channel with themself.
|
||||
- Fixed an issue where a dark theme set from webapp broke mobile display.
|
||||
- Fixed an issue where channel drawer transition sometimes lagged.
|
||||
- Fixed an issue where sending photos to Mattermost created large files.
|
||||
- Fixed an issue where the apps showed "Select a Team" screen when opened.
|
||||
- Fixed an issue where at-mention, emoji, and slash command autocompletes had a double top border.
|
||||
- Fixed an issue where the drawer was unable to close when showing the team list.
|
||||
- Fixed an issue where team sidebar showed + sign even without more teams to join.
|
||||
|
||||
|
||||
## 1.13.1 Release
|
||||
- Release Date: October 18, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue preventing some users from authenticating using OKTA
|
||||
|
||||
## v1.13.0 Release
|
||||
- Release Date: October 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
|
||||
|
||||
### Highlights
|
||||
|
||||
#### View Emoji Reactions
|
||||
- Hold down on any emoji reaction to see who reacted to the post.
|
||||
|
||||
#### Hashtags
|
||||
- Added support for searching for hashtags in posts.
|
||||
|
||||
#### Dropdown menus
|
||||
- Added support for dropdown menus in message attachments.
|
||||
|
||||
### Improvements
|
||||
- Added support for iPhone XR, XS and XS Max.
|
||||
- Added support for nicknames on user profile.
|
||||
- On servers 5.4+, added support for searching in direct and group message channels using the "in:" modifier.
|
||||
- Channel autocomplete now gets closed if multiple tildes are typed.
|
||||
- Added a draft icon in sidebar and channel switcher for channels with unsent messages.
|
||||
- Users are now redirected to the archived channel view (rather than to Town Square) when a channel is archived.
|
||||
- When closing an archived channel, users are now returned to the previously viewed channel.
|
||||
|
||||
### Bug Fixes
|
||||
- Refactored postlist to include Android Pie fixes and smoother scrolling.
|
||||
- Fixed an issue where deactivated users were not marked as such in "Jump To" search.
|
||||
- Fixed an issue where users got a permission error when trying to open a file from within the image preview screen.
|
||||
- Fixed an issue where session expiry notifications were not being sent on Android.
|
||||
- Fixed an issue where post attachments failed to upload.
|
||||
- Fixed an issue where the "DM More..." list cut off user info.
|
||||
- Fixed an issue where the user would briefly see a system message when loading a reply thread.
|
||||
- Fixed an issue where the error message was incorrectly formatted if the login method was set to email/password and the user tried to log in with SAML.
|
||||
- Fixed an issue on Android where the keyboard sometimes overlapped the bottom of the post textbox.
|
||||
- Fixed an issue where there was no option to take video via "+" > "Take Photo or Video" on iOS.
|
||||
|
||||
## v1.12.0 Release
|
||||
- Release Date: September 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Search Date Filters
|
||||
- Search for messages before, on, or after a specified date.
|
||||
|
||||
### Improvements
|
||||
- Added notification support for Android O and P.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Okta was not able to login in some deployments.
|
||||
- Fixed an issue where messages in Direct Message channels did not show when clicking "Jump To".
|
||||
- Fixed an issue where `Show More` on a post with a message attachment displayed a blank where content should have been.
|
||||
- Prevent downloading of files when disallowed in the System Console.
|
||||
- Fixed an issue where users could not click on attachment filenames to open them.
|
||||
- Fixed an issue where email notification settings did not save from mobile.
|
||||
- Fixed an issue where the share extension allowed users to select and attempt to share content to channels that had been archived.
|
||||
- Fixed an issue where reacting to an existing emoji in an archived channel was allowed.
|
||||
- Fixed an issue where archived channels sometimes remained in the drawer.
|
||||
- Fixed an issue where deactivated users were not marked as such in Direct Message search.
|
||||
|
||||
|
||||
## v1.11.0 Release
|
||||
- Release Date: August 16, 2018
|
||||
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Searching Archived Channels
|
||||
- Added ability to search for archived channels. Requires Mattermost server v5.2 or later.
|
||||
|
||||
#### Deep Linking
|
||||
- Added the ability for custom builds to open Mattermost links directly in the app rather than the default mobile browser. Learn more in our [documentation](https://docs.mattermost.com/mobile/mobile-faq.html#how-do-i-configure-deep-linking)
|
||||
|
||||
### Improvements
|
||||
- Added profile pop-up to combined system messages.
|
||||
- Force re-entering SSO auth credentials after logout.
|
||||
- Added consecutive posts by the same user.
|
||||
- Added a loading indicator when user info is still loading in the left-hand side.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where Android devices showed an incorrect timestamp.
|
||||
- Fixed an issue on Android where the app did not get sent to the background when pressing the hardware back button in the channel screen.
|
||||
- Fixed an issue with video playback when the filename had spaces.
|
||||
- Fixed an issue where the app crashed when playing YouTube videos.
|
||||
- Fixed an issue with session expiration notification.
|
||||
- Fixed an issue with sharing files from Google Drive in Android Share Extension.
|
||||
- Fixed an issue on Android where replying to a push notification sometimes went to the wrong channel.
|
||||
- Fixed an issue where the previous server URL was present on the input textbox before changing the screen to Login.
|
||||
- Fixed an issue where user menu was not translated correctly.
|
||||
- Fixed an issue where some field lengths in Account Settings didn't match the desktop app.
|
||||
- Fixed an issue where long URLs for embedded images in message attachments got cut off and didn't render.
|
||||
- Fixed an issue where link preview images were not cropped properly.
|
||||
- Fixed an issue where long usernames didn't wrap properly in the Account Settings menu.
|
||||
- Fixed an issue where DMs would not open if users were using "Jump To".
|
||||
- Fixed an issue where no message was displayed after removing a user from a channel with join/leave messages disabled.
|
||||
|
||||
## v1.10.0 Release
|
||||
- Release Date: July 16, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Channel drawer performance
|
||||
- Android devices will notice significant performance improvements when opening and closing the channel drawer.
|
||||
|
||||
#### Channel loading performance
|
||||
- Improved channel loading performance as post are retrieved with every push notification
|
||||
|
||||
#### Announcement banner improvements
|
||||
- Markdown now renders when announcement banners are expanded
|
||||
- When enabled by the System Admin, users can now dismiss announcement banners until their next session
|
||||
|
||||
### Improvements
|
||||
|
||||
- Combined consecutive messages from the same user.
|
||||
- Added experimental support for certificate-based authentication (CBA) for iOS to identify a user or a device before granting access to Mattermost. See [documentation](https://docs.mattermost.com/deployment/certificate-based-authentication.html) to learn more.
|
||||
- Added support for the experimental automatic direct message replies feature.
|
||||
- Added support for the experimental timezone feature.
|
||||
- Changed post textbox to not be a connected component.
|
||||
- Allow connecting to mattermost instances hosted at subpaths.
|
||||
- Added support for starting YouTube videos at a given time.
|
||||
- Added support for keeping messages if slash command fails.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed an issue where the unread badge background was always white.
|
||||
- Fixed an issue where a username repeated in system message if user was added to a channel more than once.
|
||||
- Fixed an issue where Android Sharing from Microsoft apps failed.
|
||||
- Fixed an issue where YouTube crashed the app if link did not have a time set.
|
||||
- Fixed an issue where System Admins did not see all teams available to join on mobile.
|
||||
- Fixed an issue where users were unable to share from Files app.
|
||||
- Fixed an issue where viewing a non-existent permalink didn't show an error message.
|
||||
- Fixed an issue where jumping to a channel search did not bold unread channels.
|
||||
- Fixed an issue with being able to add own user to a Group Message channel.
|
||||
- Fixed an issue with not being able to reply from a push notification on iOS.
|
||||
- Fixed an issue where the app did not display Brazilian language.
|
||||
|
||||
## 1.9.3 Release
|
||||
- Release Date: July 04, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed multiple issues causing app crashes
|
||||
- Fixed an issue on iOS devices with typing non-english characters in the post input box
|
||||
|
||||
## 1.9.2 Release
|
||||
- Release Date: June 27, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed an issue where attached videos did not play for the poster
|
||||
- Fixed an issue where "Jump to recent messages" from the permalink view did not direct the user to the bottom of the channel
|
||||
- Fixed an issue where post comments did not identify which parent post they belonged to
|
||||
- Fixed multiple issues with typing non-english characters in the post input box
|
||||
- Fixed multiple issues causing random app crashes
|
||||
- Fixed an issue where files from the Android Files app failed to upload
|
||||
- Fixed an issue where the iOS share extension crashed when switching the team or channel
|
||||
- Fixed an issue where files from the Microsoft app failed to upload
|
||||
- Fixed an issue on Android devices where sharing files changed the file extension of the attachment
|
||||
|
||||
## 1.9.1 Release
|
||||
- Release Date: June 23, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue with typing lag on Android devices
|
||||
- Fixed an issue causing users to be logged out after upgrading to v1.9.0
|
||||
- Fixed an issue where the ``in:`` and ``from:`` modifiers were not being added to the search field
|
||||
|
||||
## v1.9.0 Release
|
||||
- Release Date: June 16, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Improved first load time on Android
|
||||
- Significantly decreased first load time on Android devices from cold start.
|
||||
|
||||
#### iOS Files app support
|
||||
- Added support for attaching files from the iOS Files app from within Mattermost.
|
||||
|
||||
#### Improved styling of push notification
|
||||
- Improved the layout of message content, channel name and sender name in push notifications.
|
||||
|
||||
### Improvements
|
||||
|
||||
- Combined join/leave system messages.
|
||||
- Added splash screen and channel loader improvements.
|
||||
- Removed the desktop notification duration setting.
|
||||
- Added cache team icon and set background to always be white if using a PNG file.
|
||||
- Added whitelabel for icons and splash screen.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed an issue where other user's display name did not render in combined system messages after joining the channel.
|
||||
- Fixed an issue where posts incorrectly had "Commented on Someone's message" above them.
|
||||
- Fixed an issue where deleting a post or its parent in permalink view left permalink view blank.
|
||||
- Fixed an issue where "User is typing" message cut was off.
|
||||
- Fixed an issue where `More New Messages Above` appeared at the top of new channel on joining.
|
||||
- Fixed an issue where a user was not directed to Town Square when leaving a channel.
|
||||
- Fixed an issue where long post were not collapsed on Android.
|
||||
- Fixed an issue where a user's name was initially shown as "someone" when opening a direct message with the user.
|
||||
- Fixed an issue where an error was received when trying to change the team or channel from the share extension.
|
||||
- Fixed an issue where switching to a newly created channel from a push notification redirected a user to Town Square.
|
||||
- Fixed an issue where a public channel made private did not disappear automatically from clients not part of the channel.
|
||||
|
||||
## v1.8.0 Release
|
||||
- Release Date: April 27, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Image performance
|
||||
- Images are now downloaded and stored locally for better performance
|
||||
|
||||
#### Flagged Posts and Recent Mentions
|
||||
- Access all your flagged posts and recent mentions from the buttons in the sidebar
|
||||
|
||||
#### Muted Channels
|
||||
- Added support for Muted Channels released with Mattermost server v4.9
|
||||
|
||||
### Improvements
|
||||
- Date separators now appear between each posts in the search view
|
||||
- Deactivated users are now filtered out of the channel members lists
|
||||
- Direct Messages user list is now sorted by username first
|
||||
- Added the option to Direct Message yourself from your user profile screen
|
||||
- Improved performance on the post list
|
||||
- Improved matching and display when searching for users in the Direct Message user list
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where emoji reactions could be added from the search view but did not appear
|
||||
- Fixed an issue causing the app to crash when trying to share content from a custom keyboard
|
||||
- Fixed an issue where team names were being sorted based on letter case
|
||||
- Fixed an issue where username would not be inserted to the post draft when using experimental configuration settings
|
||||
- Fixed an issue with nested bullet lists being cut off in the user interface
|
||||
- Fixed an issue where private channels were listed in the public channels section of the channel autocomplete list
|
||||
- Fixed an issue where a profile images could not be updated from the app
|
||||
|
||||
## v1.7.1 Release
|
||||
- Release Date: April 3, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue where the iOS share extension sometimes crashed the Mattermost app
|
||||
- Fixed an issue preventing Markdown tables from rendering with some international characters
|
||||
|
||||
## v1.7.0 Release
|
||||
- Release Date: March 26, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### iOS File Sharing
|
||||
- Share files and images from other applications as attached files in Mattermost
|
||||
|
||||
#### Markdown Tables
|
||||
- Tables created using markdown formatting can now be viewed in the app
|
||||
|
||||
#### Permalinks
|
||||
- Permalinks now open in the app instead of launching a browser window
|
||||
|
||||
### Improvements
|
||||
- Increased the tappable area of various icons for improved usability
|
||||
- Announcement banners now display in the app
|
||||
- Added "+" button to add emoji reactions to a post
|
||||
- Minor performance improvements for app launch time
|
||||
- Text files can now be viewed in the app
|
||||
- Support for email autolinking into the app
|
||||
|
||||
### Bugs
|
||||
- Fixed an issue causing some devices to hang at the splash screen on app launch
|
||||
- Fixed an issue causing some letters to be hidden in the Android search input box
|
||||
- Fixed an issue causing some Direct Message channels to show date stamps below the most recent message
|
||||
- Fixed an issue where users weren't able to join open teams they've never been a member of
|
||||
- Fixed an issue so double tapping buttons can no longer cause UI issues
|
||||
- Fixed an issue where changing the channel display name wasn't being updated in the UI appropriately
|
||||
- Fixed an issue where searhing for public channels sometimes showed no results
|
||||
- Fixed an issue where the post menu could remain open while scrolling in the post list
|
||||
- Fixed an issue where the system message to add users to a channel was missing the execution link
|
||||
- Fixed an issue where bulleted lists cut off text if nested deeper than two levels
|
||||
- Fixed an issue where logging into an account that is not on any team freezes the app
|
||||
- Fixed an issue on iOS causing the app to crash when taking a photo then attaching it to a post
|
||||
|
||||
## v1.6.1 Release
|
||||
- Release Date: February 13, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue preventing the app from going to the correct channel when opened from a push notification
|
||||
- Fixed an issue on Android devices where the app could sometimes freeze on the launch screen
|
||||
- Fixed an issue on Samsung devices causing extra letters to be insterted when typing to filter user lists
|
||||
|
||||
## v1.6.0 Release
|
||||
- Release Date: February 6, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### Android File Sharing
|
||||
- Share files and images from other applications as attached files in Mattermost
|
||||
|
||||
### Improvements
|
||||
- Added a right drawer to access settings, edit profile information, change online status and logout
|
||||
- Added support for opening a Direct Message channel with yourself
|
||||
|
||||
### Bugs
|
||||
- Fixed a number of issues causing crashes on Android devices
|
||||
- Fixed an issue with auto capitalization on Android keyboards
|
||||
- Fixed an issue where the GitLab SSO login button sometimes didn't appear
|
||||
- Fixed an issue with link previews not appearing on some accounts
|
||||
- Fixed an issue where logging out of the app didn't clear the notification badge on the homescreen icon
|
||||
- Fixed an issue where interactive message buttons would not wrap to a new line
|
||||
- Fixed an issue where the keyboard would sometimes overlap the text input box
|
||||
- Fixed an issue where the Direct Message channel wouldn't open from the profile page
|
||||
- Fixed an issue where posts would sometimes overlap
|
||||
- Fixed an issue where the app sometimes hangs on logout
|
||||
|
||||
## v1.5.3 Release
|
||||
- Release Date: February 1, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
- Fixed a login issue when connecting to servers running a Data Retention policy
|
||||
|
||||
## v1.5.2 Release
|
||||
- Release Date: January 12, 2018
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue causing some Android devices to crash on launch
|
||||
- Fixed an issue with the app occasionally crashing when receiving push notifications in a new channel
|
||||
- Channel footer area is now refreshed when switching between Group and Direct Message channels
|
||||
- Fixed an issue on some Android devices so Mattermost verifies it has permissions to access ringtones
|
||||
- Fixed an issue where the text box overlapped the keyboard on some iOS devices using multiple keyboard layouts
|
||||
- Fixed an issue with video uploads on Android devices
|
||||
- Fixed an issue with GIF uploads on iOS devices
|
||||
- Fixed an issue with the mention badge flickering on the channel drawer icon when there were over 10 unread mentions
|
||||
- Fixed an issue with the app occasionally freezing when requesting the RefreshToken
|
||||
|
||||
## v1.5.1 Release
|
||||
|
||||
- Release Date: December 7, 2017
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an issue with the upgrade app screen showing with a transparent background
|
||||
- Fixed an issue with clearing or replying to notifications sometimes crashing the app on Android
|
||||
- Fixed an issue with the app sometimes crashing due to a missing function in the swiping control
|
||||
|
||||
## v1.5 Release
|
||||
|
||||
- Release Date: December 6, 2017
|
||||
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
|
||||
|
||||
### Highlights
|
||||
|
||||
#### File Viewer
|
||||
- Preview videos, RTF, PDFs, Word, Excel, and Powerpoint files
|
||||
|
||||
#### iPhone X Compatibility
|
||||
- Added support for iPhone X
|
||||
|
||||
#### Slash Commands
|
||||
- Added support for using custom slash commands
|
||||
- Added support for built-in slash commands /away, /online, /offline, /dnd, /header, /purpose, /kick, /me, /shrug
|
||||
|
||||
### Improvements
|
||||
- In iOS, 3D touch can now be used to peek into a channel to view the contents, and quickly mark it as read
|
||||
- Markdown images in posts now render
|
||||
- Copy posts, URLs, and code blocks
|
||||
- Opening a channel with Unread messages takes you to the "New Messages" indicator
|
||||
- Support for data retention, interactive message buttons, and viewing Do Not Disturb statuses depending on the server version
|
||||
- (Edited) indicator now shows up beside edited posts
|
||||
- Added a "Recently Used" section for emoji reactions
|
||||
|
||||
### Bug Fixes
|
||||
- Android notifications now follow the default system setting for vibration
|
||||
- Fixed app crashing when opening notification settings on Android
|
||||
- Fixed an issue where the "Proceed" button on sign in screen stopped working after pressing logout multiple times
|
||||
- HEIC images posted from iPhones now get converted to JPEG before uploading
|
||||
|
||||
## v1.4.1 Release
|
||||
|
||||
Release Date: Nov 15, 2017
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# Code Contribution Guidelines
|
||||
|
||||
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
|
||||
Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects.
|
||||
|
||||
Note: Community work won't start until October 31, and no community pull requests will be accepted before then.
|
||||
|
||||
### Review Process for this Repo
|
||||
|
||||
After following the steps in the [Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html), submitted pull requests go through the review process outlined below. We aim to start reviewing pull requests in this repo the week they are submitted, but the length of time to complete the process will vary depending on the pull request.
|
||||
|
||||
The one exception may be around release time, where the review process may take longer as the team focuses on our [release process](https://docs.mattermost.com/process/release-process.html).
|
||||
|
||||
#### `Stage 1: PM Review`
|
||||
|
||||
A Product Manager will review the pull request to make sure it:
|
||||
|
||||
1. Fits with our product roadmap
|
||||
2. Works as expected
|
||||
3. Meets UX guidelines
|
||||
|
||||
This step is sometimes skipped for bugs or small improvements with a ticket, but always happens for new features or pull requests without a related ticket.
|
||||
|
||||
The Product Manager may come back with some bugs or UI improvements to fix before the pull request moves on to the next stage.
|
||||
|
||||
#### `Stage 2: Dev Review`
|
||||
|
||||
Two developers will review the pull request and either give feedback or `+1` the PR.
|
||||
|
||||
Any comments will need to be addressed before the pull request moves on to the last stage.
|
||||
|
||||
- PRs that do not follow Style Guides cannot be merged
|
||||
|
||||
#### `Stage 3: Ready to Merge`
|
||||
|
||||
The review process is complete, and the pull request will be merged.
|
||||
|
||||
When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/).
|
||||
|
||||
729
ImagePickerModule.java
Normal file
@@ -0,0 +1,729 @@
|
||||
package com.imagepicker;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Patterns;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.bridge.ActivityEventListener;
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.imagepicker.media.ImageConfig;
|
||||
import com.imagepicker.permissions.PermissionUtils;
|
||||
import com.imagepicker.permissions.OnImagePickerPermissionsCallback;
|
||||
import com.imagepicker.utils.MediaUtils.ReadExifResult;
|
||||
import com.imagepicker.utils.RealPathUtil;
|
||||
import com.imagepicker.utils.UI;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
import com.facebook.react.modules.core.PermissionAwareActivity;
|
||||
|
||||
import static com.imagepicker.utils.MediaUtils.*;
|
||||
import static com.imagepicker.utils.MediaUtils.createNewFile;
|
||||
import static com.imagepicker.utils.MediaUtils.getResizedImage;
|
||||
|
||||
public class ImagePickerModule extends ReactContextBaseJavaModule
|
||||
implements ActivityEventListener
|
||||
{
|
||||
|
||||
public static final int REQUEST_LAUNCH_IMAGE_CAPTURE = 13001;
|
||||
public static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002;
|
||||
public static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003;
|
||||
public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004;
|
||||
public static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001;
|
||||
public static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002;
|
||||
|
||||
private final ReactApplicationContext reactContext;
|
||||
private final int dialogThemeId;
|
||||
|
||||
protected Callback callback;
|
||||
private ReadableMap options;
|
||||
protected Uri cameraCaptureURI;
|
||||
private Boolean noData = false;
|
||||
private Boolean pickVideo = false;
|
||||
private ImageConfig imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
|
||||
|
||||
@Deprecated
|
||||
private int videoQuality = 1;
|
||||
|
||||
@Deprecated
|
||||
private int videoDurationLimit = 0;
|
||||
|
||||
private ResponseHelper responseHelper = new ResponseHelper();
|
||||
private PermissionListener listener = new PermissionListener()
|
||||
{
|
||||
public boolean onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults)
|
||||
{
|
||||
boolean permissionsGranted = true;
|
||||
for (int i = 0; i < permissions.length; i++)
|
||||
{
|
||||
final boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
|
||||
permissionsGranted = permissionsGranted && granted;
|
||||
}
|
||||
|
||||
if (callback == null || options == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!permissionsGranted)
|
||||
{
|
||||
responseHelper.invokeError(callback, "Permissions weren't granted");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (requestCode)
|
||||
{
|
||||
case REQUEST_PERMISSIONS_FOR_CAMERA:
|
||||
launchCamera(options, callback);
|
||||
break;
|
||||
|
||||
case REQUEST_PERMISSIONS_FOR_LIBRARY:
|
||||
launchImageLibrary(options, callback);
|
||||
break;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public ImagePickerModule(ReactApplicationContext reactContext,
|
||||
@StyleRes final int dialogThemeId)
|
||||
{
|
||||
super(reactContext);
|
||||
|
||||
this.dialogThemeId = dialogThemeId;
|
||||
this.reactContext = reactContext;
|
||||
this.reactContext.addActivityEventListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "ImagePickerManager";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showImagePicker(final ReadableMap options, final Callback callback) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity == null)
|
||||
{
|
||||
responseHelper.invokeError(callback, "can't find current Activity");
|
||||
return;
|
||||
}
|
||||
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
|
||||
|
||||
final AlertDialog dialog = UI.chooseDialog(this, options, new UI.OnAction()
|
||||
{
|
||||
@Override
|
||||
public void onTakePhoto(@NonNull final ImagePickerModule module)
|
||||
{
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
module.launchCamera();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUseLibrary(@NonNull final ImagePickerModule module)
|
||||
{
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
module.launchImageLibrary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(@NonNull final ImagePickerModule module)
|
||||
{
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
module.doOnCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomButton(@NonNull final ImagePickerModule module,
|
||||
@NonNull final String action)
|
||||
{
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
module.invokeCustomButton(action);
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void doOnCancel()
|
||||
{
|
||||
if (this.callback != null) {
|
||||
responseHelper.invokeCancel(this.callback);
|
||||
}
|
||||
}
|
||||
|
||||
public void launchCamera()
|
||||
{
|
||||
this.launchCamera(this.options, this.callback);
|
||||
}
|
||||
|
||||
// NOTE: Currently not reentrant / doesn't support concurrent requests
|
||||
@ReactMethod
|
||||
public void launchCamera(final ReadableMap options, final Callback callback)
|
||||
{
|
||||
if (!isCameraAvailable())
|
||||
{
|
||||
responseHelper.invokeError(callback, "Camera not available");
|
||||
return;
|
||||
}
|
||||
|
||||
final Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null)
|
||||
{
|
||||
responseHelper.invokeError(callback, "can't find current Activity");
|
||||
return;
|
||||
}
|
||||
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
|
||||
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
parseOptions(this.options);
|
||||
|
||||
int requestCode;
|
||||
Intent cameraIntent;
|
||||
|
||||
if (pickVideo)
|
||||
{
|
||||
requestCode = REQUEST_LAUNCH_VIDEO_CAPTURE;
|
||||
cameraIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, videoQuality);
|
||||
if (videoDurationLimit > 0)
|
||||
{
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, videoDurationLimit);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
requestCode = REQUEST_LAUNCH_IMAGE_CAPTURE;
|
||||
cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
|
||||
final File original = createNewFile(reactContext, this.options, false);
|
||||
imageConfig = imageConfig.withOriginalFile(original);
|
||||
|
||||
if (imageConfig.original != null) {
|
||||
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
|
||||
}else {
|
||||
responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
return;
|
||||
}
|
||||
if (cameraCaptureURI == null)
|
||||
{
|
||||
responseHelper.invokeError(callback, "Couldn't get file path for photo");
|
||||
return;
|
||||
}
|
||||
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI);
|
||||
}
|
||||
|
||||
if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null)
|
||||
{
|
||||
responseHelper.invokeError(callback, "Cannot launch camera");
|
||||
return;
|
||||
}
|
||||
|
||||
// Workaround for Android bug.
|
||||
// grantUriPermission also needed for KITKAT,
|
||||
// see https://code.google.com/p/android/issues/detail?id=76683
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
|
||||
List<ResolveInfo> resInfoList = reactContext.getPackageManager().queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
for (ResolveInfo resolveInfo : resInfoList) {
|
||||
String packageName = resolveInfo.activityInfo.packageName;
|
||||
reactContext.grantUriPermission(packageName, cameraCaptureURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
currentActivity.startActivityForResult(cameraIntent, requestCode);
|
||||
}
|
||||
catch (ActivityNotFoundException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
responseHelper.invokeError(callback, "Cannot launch camera");
|
||||
}
|
||||
}
|
||||
|
||||
public void launchImageLibrary()
|
||||
{
|
||||
this.launchImageLibrary(this.options, this.callback);
|
||||
}
|
||||
// NOTE: Currently not reentrant / doesn't support concurrent requests
|
||||
@ReactMethod
|
||||
public void launchImageLibrary(final ReadableMap options, final Callback callback)
|
||||
{
|
||||
final Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity == null) {
|
||||
responseHelper.invokeError(callback, "can't find current Activity");
|
||||
return;
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.callback = callback;
|
||||
|
||||
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
parseOptions(this.options);
|
||||
|
||||
int requestCode;
|
||||
Intent libraryIntent;
|
||||
if (pickVideo)
|
||||
{
|
||||
requestCode = REQUEST_LAUNCH_VIDEO_LIBRARY;
|
||||
libraryIntent = new Intent(Intent.ACTION_PICK);
|
||||
libraryIntent.setType("video/*");
|
||||
}
|
||||
else
|
||||
{
|
||||
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
|
||||
libraryIntent = new Intent(Intent.ACTION_PICK,
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
|
||||
}
|
||||
|
||||
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
|
||||
{
|
||||
responseHelper.invokeError(callback, "Cannot launch photo library");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
currentActivity.startActivityForResult(libraryIntent, requestCode);
|
||||
}
|
||||
catch (ActivityNotFoundException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
responseHelper.invokeError(callback, "Cannot launch photo library");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
|
||||
//robustness code
|
||||
if (passResult(requestCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
responseHelper.cleanResponse();
|
||||
|
||||
// user cancel
|
||||
if (resultCode != Activity.RESULT_OK)
|
||||
{
|
||||
removeUselessFiles(requestCode, imageConfig);
|
||||
responseHelper.invokeCancel(callback);
|
||||
callback = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = null;
|
||||
switch (requestCode)
|
||||
{
|
||||
case REQUEST_LAUNCH_IMAGE_CAPTURE:
|
||||
uri = cameraCaptureURI;
|
||||
break;
|
||||
|
||||
case REQUEST_LAUNCH_IMAGE_LIBRARY:
|
||||
uri = data.getData();
|
||||
String realPath = getRealPathFromURI(uri);
|
||||
final boolean isUrl = !TextUtils.isEmpty(realPath) &&
|
||||
Patterns.WEB_URL.matcher(realPath).matches();
|
||||
if (realPath == null || isUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
File file = createFileFromURI(uri);
|
||||
realPath = file.getAbsolutePath();
|
||||
uri = Uri.fromFile(file);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// image not in cache
|
||||
responseHelper.putString("error", "Could not read photo");
|
||||
responseHelper.putString("uri", uri.toString());
|
||||
responseHelper.invokeResponse(callback);
|
||||
callback = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
imageConfig = imageConfig.withOriginalFile(new File(realPath));
|
||||
break;
|
||||
|
||||
case REQUEST_LAUNCH_VIDEO_LIBRARY:
|
||||
responseHelper.putString("uri", data.getData().toString());
|
||||
responseHelper.putString("path", getRealPathFromURI(data.getData()));
|
||||
responseHelper.invokeResponse(callback);
|
||||
callback = null;
|
||||
return;
|
||||
|
||||
case REQUEST_LAUNCH_VIDEO_CAPTURE:
|
||||
final String path = getRealPathFromURI(data.getData());
|
||||
responseHelper.putString("uri", data.getData().toString());
|
||||
responseHelper.putString("path", path);
|
||||
fileScan(reactContext, path);
|
||||
responseHelper.invokeResponse(callback);
|
||||
callback = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final ReadExifResult result = readExifInterface(responseHelper, imageConfig);
|
||||
|
||||
if (result.error != null)
|
||||
{
|
||||
removeUselessFiles(requestCode, imageConfig);
|
||||
responseHelper.invokeError(callback, result.error.getMessage());
|
||||
callback = null;
|
||||
return;
|
||||
}
|
||||
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(imageConfig.original.getAbsolutePath(), options);
|
||||
int initialWidth = options.outWidth;
|
||||
int initialHeight = options.outHeight;
|
||||
updatedResultResponse(uri, imageConfig.original.getAbsolutePath());
|
||||
|
||||
// don't create a new file if contraint are respected
|
||||
if (imageConfig.useOriginal(initialWidth, initialHeight, result.currentRotation))
|
||||
{
|
||||
responseHelper.putInt("width", initialWidth);
|
||||
responseHelper.putInt("height", initialHeight);
|
||||
fileScan(reactContext, imageConfig.original.getAbsolutePath());
|
||||
}
|
||||
else
|
||||
{
|
||||
imageConfig = getResizedImage(reactContext, this.options, imageConfig, initialWidth, initialHeight, requestCode);
|
||||
if (imageConfig.resized == null)
|
||||
{
|
||||
removeUselessFiles(requestCode, imageConfig);
|
||||
responseHelper.putString("error", "Can't resize the image");
|
||||
}
|
||||
else
|
||||
{
|
||||
uri = Uri.fromFile(imageConfig.resized);
|
||||
BitmapFactory.decodeFile(imageConfig.resized.getAbsolutePath(), options);
|
||||
responseHelper.putInt("width", options.outWidth);
|
||||
responseHelper.putInt("height", options.outHeight);
|
||||
|
||||
updatedResultResponse(uri, imageConfig.resized.getAbsolutePath());
|
||||
fileScan(reactContext, imageConfig.resized.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
if (imageConfig.saveToCameraRoll && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|
||||
{
|
||||
final RolloutPhotoResult rolloutResult = rolloutPhotoFromCamera(imageConfig);
|
||||
|
||||
if (rolloutResult.error == null)
|
||||
{
|
||||
imageConfig = rolloutResult.imageConfig;
|
||||
uri = Uri.fromFile(imageConfig.getActualFile());
|
||||
updatedResultResponse(uri, imageConfig.getActualFile().getAbsolutePath());
|
||||
}
|
||||
else
|
||||
{
|
||||
removeUselessFiles(requestCode, imageConfig);
|
||||
final String errorMessage = new StringBuilder("Error moving image to camera roll: ")
|
||||
.append(rolloutResult.error.getMessage()).toString();
|
||||
responseHelper.putString("error", errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
responseHelper.invokeResponse(callback);
|
||||
callback = null;
|
||||
this.options = null;
|
||||
}
|
||||
|
||||
public void invokeCustomButton(@NonNull final String action)
|
||||
{
|
||||
responseHelper.invokeCustomButton(this.callback, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) { }
|
||||
|
||||
public Context getContext()
|
||||
{
|
||||
return getReactApplicationContext();
|
||||
}
|
||||
|
||||
public @StyleRes int getDialogThemeId()
|
||||
{
|
||||
return this.dialogThemeId;
|
||||
}
|
||||
|
||||
public @NonNull Activity getActivity()
|
||||
{
|
||||
return getCurrentActivity();
|
||||
}
|
||||
|
||||
|
||||
private boolean passResult(int requestCode)
|
||||
{
|
||||
return callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|
||||
|| (requestCode != REQUEST_LAUNCH_IMAGE_CAPTURE && requestCode != REQUEST_LAUNCH_IMAGE_LIBRARY
|
||||
&& requestCode != REQUEST_LAUNCH_VIDEO_LIBRARY && requestCode != REQUEST_LAUNCH_VIDEO_CAPTURE);
|
||||
}
|
||||
|
||||
private void updatedResultResponse(@Nullable final Uri uri,
|
||||
@NonNull final String path)
|
||||
{
|
||||
responseHelper.putString("uri", uri.toString());
|
||||
responseHelper.putString("path", path);
|
||||
|
||||
if (!noData) {
|
||||
responseHelper.putString("data", getBase64StringFromFile(path));
|
||||
}
|
||||
|
||||
putExtraFileInfo(path, responseHelper);
|
||||
}
|
||||
|
||||
private boolean permissionsCheck(@NonNull final Activity activity,
|
||||
@NonNull final Callback callback,
|
||||
@NonNull final int requestCode)
|
||||
{
|
||||
final int writePermission = ActivityCompat
|
||||
.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
final int cameraPermission = ActivityCompat
|
||||
.checkSelfPermission(activity, Manifest.permission.CAMERA);
|
||||
|
||||
final boolean permissionsGrated = writePermission == PackageManager.PERMISSION_GRANTED &&
|
||||
cameraPermission == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (!permissionsGrated)
|
||||
{
|
||||
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
|
||||
|
||||
if (dontAskAgain)
|
||||
{
|
||||
final AlertDialog dialog = PermissionUtils
|
||||
.explainingDialog(this, options, new PermissionUtils.OnExplainingPermissionCallback()
|
||||
{
|
||||
@Override
|
||||
public void onCancel(WeakReference<ImagePickerModule> moduleInstance,
|
||||
DialogInterface dialogInterface)
|
||||
{
|
||||
final ImagePickerModule module = moduleInstance.get();
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
module.doOnCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReTry(WeakReference<ImagePickerModule> moduleInstance,
|
||||
DialogInterface dialogInterface)
|
||||
{
|
||||
final ImagePickerModule module = moduleInstance.get();
|
||||
if (module == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
Uri uri = Uri.fromParts("package", module.getContext().getPackageName(), null);
|
||||
intent.setData(uri);
|
||||
final Activity innerActivity = module.getActivity();
|
||||
if (innerActivity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
innerActivity.startActivityForResult(intent, 1);
|
||||
}
|
||||
});
|
||||
if (dialog != null) {
|
||||
dialog.show();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
String[] PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
|
||||
if (activity instanceof ReactActivity)
|
||||
{
|
||||
((ReactActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
|
||||
}
|
||||
else if (activity instanceof OnImagePickerPermissionsCallback)
|
||||
{
|
||||
((OnImagePickerPermissionsCallback) activity).setPermissionListener(listener);
|
||||
ActivityCompat.requestPermissions(activity, PERMISSIONS, requestCode);
|
||||
}
|
||||
else if (activity instanceof PermissionAwareActivity) {
|
||||
((PermissionAwareActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
|
||||
}
|
||||
else
|
||||
{
|
||||
final String errorDescription = new StringBuilder(activity.getClass().getSimpleName())
|
||||
.append(" must implement ")
|
||||
.append(OnImagePickerPermissionsCallback.class.getSimpleName())
|
||||
.append(PermissionAwareActivity.class.getSimpleName())
|
||||
.toString();
|
||||
throw new UnsupportedOperationException(errorDescription);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isCameraAvailable() {
|
||||
return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
|
||||
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|
||||
}
|
||||
|
||||
private @NonNull String getRealPathFromURI(@NonNull final Uri uri) {
|
||||
return RealPathUtil.getRealPathFromURI(reactContext, uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file from uri to allow image picking of image in disk cache
|
||||
* (Exemple: facebook image, google image etc..)
|
||||
*
|
||||
* @doc =>
|
||||
* https://github.com/nostra13/Android-Universal-Image-Loader#load--display-task-flow
|
||||
*
|
||||
* @param uri
|
||||
* @return File
|
||||
* @throws Exception
|
||||
*/
|
||||
private File createFileFromURI(Uri uri) throws Exception {
|
||||
File file = new File(reactContext.getExternalCacheDir(), "photo-" + uri.getLastPathSegment());
|
||||
InputStream input = reactContext.getContentResolver().openInputStream(uri);
|
||||
OutputStream output = new FileOutputStream(file);
|
||||
|
||||
try {
|
||||
byte[] buffer = new byte[4 * 1024];
|
||||
int read;
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
output.flush();
|
||||
} finally {
|
||||
output.close();
|
||||
input.close();
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getBase64StringFromFile(String absoluteFilePath) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = new FileInputStream(new File(absoluteFilePath));
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
byte[] bytes;
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
try {
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
bytes = output.toByteArray();
|
||||
return Base64.encodeToString(bytes, Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
private void putExtraFileInfo(@NonNull final String path,
|
||||
@NonNull final ResponseHelper responseHelper)
|
||||
{
|
||||
// size && filename
|
||||
try {
|
||||
File f = new File(path);
|
||||
responseHelper.putDouble("fileSize", f.length());
|
||||
responseHelper.putString("fileName", f.getName());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// type
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
|
||||
if (extension != null) {
|
||||
responseHelper.putString("type", MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension));
|
||||
}
|
||||
}
|
||||
|
||||
private void parseOptions(final ReadableMap options) {
|
||||
noData = false;
|
||||
if (options.hasKey("noData")) {
|
||||
noData = options.getBoolean("noData");
|
||||
}
|
||||
imageConfig = imageConfig.updateFromOptions(options);
|
||||
pickVideo = false;
|
||||
if (options.hasKey("mediaType") && options.getString("mediaType").equals("video")) {
|
||||
pickVideo = true;
|
||||
}
|
||||
videoQuality = 1;
|
||||
if (options.hasKey("videoQuality") && options.getString("videoQuality").equals("low")) {
|
||||
videoQuality = 0;
|
||||
}
|
||||
videoDurationLimit = 0;
|
||||
if (options.hasKey("durationLimit")) {
|
||||
videoDurationLimit = options.getInt("durationLimit");
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Jenkinsfile
vendored
@@ -1,14 +0,0 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
stages {
|
||||
stage('Test') {
|
||||
steps {
|
||||
echo 'assets/base/config.json'
|
||||
sh 'cat assets/base/config.json'
|
||||
sh 'touch .podinstall'
|
||||
sh 'make test || exit 1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
Copyright 2016 Mattermost, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
||||
330
Makefile
@@ -1,37 +1,24 @@
|
||||
.PHONY: pre-run pre-build clean
|
||||
.PHONY: check-style
|
||||
.PHONY: start stop
|
||||
.PHONY: run run-ios run-android
|
||||
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
|
||||
.PHONY: build-pr can-build-pr prepare-pr
|
||||
.PHONY: test help
|
||||
.PHONY: run run-ios run-android check-style test clean post-install start stop
|
||||
.PHONY: check-ios-target build-ios
|
||||
.PHONY: check-android-target prepare-android-build build-android
|
||||
.PHONY: start-packager stop-packager
|
||||
|
||||
POD := $(shell which pod 2> /dev/null)
|
||||
OS := $(shell sh -c 'uname -s 2>/dev/null')
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
MM_UTILITIES_DIR = ../mattermost-utilities
|
||||
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
|
||||
android_target := $(filter-out build-android,$(MAKECMDGOALS))
|
||||
POD := $(shell command -v pod 2> /dev/null)
|
||||
|
||||
node_modules: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
.yarninstall: package.json
|
||||
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
|
||||
@echo "yarn is not installed https://yarnpkg.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm install
|
||||
@yarn install --pure-lockfile
|
||||
|
||||
npm-ci: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@echo Getting Javascript dependencies
|
||||
@npm ci
|
||||
@touch $@
|
||||
|
||||
.podinstall:
|
||||
ifeq ($(OS), Darwin)
|
||||
ifdef POD
|
||||
@echo Getting Cocoapods dependencies;
|
||||
@cd ios && pod install;
|
||||
@@ -39,10 +26,13 @@ else
|
||||
@echo "Cocoapods is not installed https://cocoapods.org/"
|
||||
@exit 1
|
||||
endif
|
||||
endif
|
||||
|
||||
@touch $@
|
||||
|
||||
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
|
||||
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
|
||||
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
|
||||
@mkdir -p dist
|
||||
|
||||
@if [ -e dist/assets ] ; then \
|
||||
@@ -52,18 +42,63 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
|
||||
@echo "Generating app assets"
|
||||
@node scripts/make-dist-assets.js
|
||||
|
||||
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
|
||||
pre-run: | .yarninstall .podinstall dist/assets
|
||||
|
||||
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
|
||||
run: run-ios
|
||||
|
||||
check-style: node_modules ## Runs eslint
|
||||
start: | pre-run start-packager
|
||||
|
||||
stop: stop-packager
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell command -v xcodebuild) ]; then \
|
||||
@echo "xcode is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v watchman) ]; then \
|
||||
@echo "watchman is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
|
||||
run-ios: | check-device-ios start-build-packager
|
||||
@echo Running iOS app in development
|
||||
@react-native run-ios --simulator="${SIMULATOR}"
|
||||
|
||||
check-device-android:
|
||||
@if ! [ $(ANDROID_HOME) ]; then \
|
||||
@echo "ANDROID_HOME is not set"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
|
||||
@echo "adb is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
ifneq ($(shell adb get-state),device)
|
||||
@echo "no android device or emulator is running"
|
||||
@exit 1;
|
||||
endif
|
||||
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
|
||||
@echo "watchman is not installed"; \
|
||||
@exit 1; \
|
||||
fi
|
||||
|
||||
run-android: | check-device-android start-build-packager prepare-android-build
|
||||
@echo Running Android app in development
|
||||
@react-native run-android --no-packager
|
||||
|
||||
test: | pre-run check-style
|
||||
@yarn test
|
||||
|
||||
check-style: .yarninstall
|
||||
@echo Checking for style guide compliance
|
||||
@npm run check
|
||||
@node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .
|
||||
|
||||
clean: ## Cleans dependencies, previous builds and temp files
|
||||
clean:
|
||||
@echo Cleaning started
|
||||
|
||||
@yarn cache clean
|
||||
@rm -rf node_modules
|
||||
@rm -f .yarninstall
|
||||
@rm -f .podinstall
|
||||
@rm -rf dist
|
||||
@rm -rf ios/build
|
||||
@@ -73,15 +108,11 @@ clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
|
||||
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
|
||||
|
||||
@# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
|
||||
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
|
||||
|
||||
@# Need to copy custom RNCNetInfo.m that checks for internet connectivity instead of reaching a host by default
|
||||
@cp ./native_modules/RNCNetInfo.m node_modules/@react-native-community/netinfo/ios/RNCNetInfo.m
|
||||
|
||||
@./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
|
||||
@# Must remove the .babelrc for 0.42.0 to work correctly
|
||||
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
|
||||
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
|
||||
@cp ./ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
|
||||
@rm -f node_modules/intl/.babelrc
|
||||
@# Hack to get react-intl and its dependencies to work with react-native
|
||||
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
|
||||
@@ -89,180 +120,111 @@ post-install:
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
|
||||
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
|
||||
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
|
||||
@sed -i'' -e "s|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java
|
||||
@sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle
|
||||
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
|
||||
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
|
||||
@sed -i'' -e 's^getItemLayout || index <= this._highestMeasuredFrameIndex,^!getItemLayout || index !== -1,^g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
|
||||
@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
|
||||
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
|
||||
@cd ./node_modules/mattermost-redux && yarn run build
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
$(call start_packager)
|
||||
|
||||
stop: ## Stops the React Native packager server
|
||||
$(call stop_packager)
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell which xcodebuild) ]; then \
|
||||
echo "xcode is not installed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell which watchman) ]; then \
|
||||
echo "watchman is not installed"; \
|
||||
exit 1; \
|
||||
start-packager:
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
|
||||
fi
|
||||
|
||||
check-device-android:
|
||||
@if ! [ $(ANDROID_HOME) ]; then \
|
||||
echo "ANDROID_HOME is not set"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if ! [ $(shell which adb 2> /dev/null) ]; then \
|
||||
echo "adb is not installed"; \
|
||||
exit 1; \
|
||||
start-build-packager:
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
node ./node_modules/react-native/local-cli/cli.js start --reset-cache & echo $$! > server.PID; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
|
||||
fi
|
||||
|
||||
@echo "Connect your Android device or open the emulator"
|
||||
@adb wait-for-device
|
||||
|
||||
@if ! [ $(shell which watchman 2> /dev/null) ]; then \
|
||||
echo "watchman is not installed"; \
|
||||
exit 1; \
|
||||
stop-packager:
|
||||
@echo Stopping React Native packager server
|
||||
@if [ -e "server.PID" ] ; then \
|
||||
kill -9 `cat server.PID` && rm server.PID; \
|
||||
echo React Native packager server stopped; \
|
||||
else \
|
||||
echo No React Native packager server running; \
|
||||
fi
|
||||
|
||||
check-ios-target:
|
||||
ifeq ($(ios_target), )
|
||||
@echo No target set to build iOS app
|
||||
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
ifneq ($(ios_target), $(filter $(ios_target),dev beta release))
|
||||
@echo Invalid target set to build iOS app
|
||||
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
do-build-ios:
|
||||
@echo "Building ios $(ios_target) app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios $(ios_target)
|
||||
|
||||
|
||||
build-ios: | check-ios-target pre-run check-style start-build-packager do-build-ios stop-packager
|
||||
|
||||
check-android-target:
|
||||
ifeq ($(android_target), )
|
||||
@echo No target set to build Android app
|
||||
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
ifneq ($(android_target), $(filter $(android_target),dev alpha release))
|
||||
@echo Invalid target set to build Android app
|
||||
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
prepare-android-build:
|
||||
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
|
||||
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
|
||||
@rm -rf ./node_modules/react-native-orientation/demo/
|
||||
|
||||
run: run-ios ## alias for run-ios
|
||||
do-build-android:
|
||||
@echo "Building android $(android_target) app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android $(android_target)
|
||||
|
||||
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running iOS app in development; \
|
||||
if [ ! -z "${SIMULATOR}" ]; then \
|
||||
react-native run-ios --simulator="${SIMULATOR}"; \
|
||||
else \
|
||||
react-native run-ios; \
|
||||
fi; \
|
||||
fi
|
||||
build-android: | check-android-target pre-run check-style start-build-packager prepare-android-build do-build-android stop-packager
|
||||
|
||||
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
wait; \
|
||||
else \
|
||||
echo Running Android app in development; \
|
||||
if [ ! -z ${VARIANT} ]; then \
|
||||
react-native run-android --no-packager --variant=${VARIANT}; \
|
||||
else \
|
||||
react-native run-android --no-packager; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
build: | stop pre-build check-style ## Builds the app for Android & iOS
|
||||
$(call start_packager)
|
||||
@echo "Building App"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
|
||||
$(call stop_packager)
|
||||
|
||||
|
||||
build-ios: | stop pre-build check-style ## Builds the iOS app
|
||||
$(call start_packager)
|
||||
@echo "Building iOS app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
$(call stop_packager)
|
||||
|
||||
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
|
||||
$(call start_packager)
|
||||
@echo "Building Android app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
|
||||
$(call start_packager)
|
||||
do-unsigned-ios:
|
||||
@echo "Building unsigned iOS app"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
@mkdir -p build-ios
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
|
||||
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
|
||||
@mv build-ios/Mattermost-unsigned.ipa .
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.ipa os_type:iOS
|
||||
@rm -rf build-ios/
|
||||
$(call stop_packager)
|
||||
|
||||
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
|
||||
$(call start_packager)
|
||||
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
@mkdir -p build-ios
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
|
||||
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
|
||||
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
|
||||
@rm -rf build-ios/
|
||||
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-simulator-x86_64.app.zip os_type:iOS
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
|
||||
$(call start_packager)
|
||||
do-unsigned-android:
|
||||
@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)
|
||||
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
unsigned-android: pre-run check-style start-build-packager do-unsigned-android stop-packager
|
||||
|
||||
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
|
||||
$(call start_packager)
|
||||
@echo "Building App from PR ${PR_ID}"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
|
||||
$(call stop_packager)
|
||||
unsigned-ios: pre-run check-style start-build-packager do-unsigned-ios stop-packager
|
||||
|
||||
can-build-pr:
|
||||
@if [ -z ${PR_ID} ]; then \
|
||||
echo a PR number needs to be specified; \
|
||||
exit 1; \
|
||||
fi
|
||||
alpha:
|
||||
@:
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
npm run mmjstool -- i18n extract-mobile
|
||||
dev:
|
||||
@:
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
beta:
|
||||
@:
|
||||
|
||||
define start_packager
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define stop_packager
|
||||
@echo Stopping React Native packager server
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
|
||||
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
|
||||
echo React Native packager server stopped; \
|
||||
else \
|
||||
echo No React Native packager server running; \
|
||||
fi
|
||||
endef
|
||||
release:
|
||||
@:
|
||||
|
||||
3766
NOTICE.txt
@@ -1,4 +1,4 @@
|
||||
Please make sure you've read the [pull request](https://developers.mattermost.com/contribute/getting-started/contribution-checklist/) section of our [code contribution guidelines](https://developers.mattermost.com/contribute/getting-started/).
|
||||
Please make sure you've read the [pull request](http://docs.mattermost.com/developer/contribution-guide.html#preparing-a-pull-request) section of our [code contribution guidelines](http://docs.mattermost.com/developer/contribution-guide.html).
|
||||
|
||||
When filling in a section please remove the help text and the above text.
|
||||
|
||||
|
||||
123
README.md
@@ -1,17 +1,16 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Supported Server versions:** 4.10+
|
||||
- **Supported iOS versions:** 10.3+
|
||||
- **Supported Android versions:** 7.0+
|
||||
**Supported Server Versions:** 4.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
**Supported iOS versions:** 9.3+
|
||||
**Supported Android versions:** 5.0+
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
|
||||
|
||||
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
|
||||
|
||||
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
|
||||
|
||||
# How to Contribute
|
||||
|
||||
### Testing
|
||||
@@ -19,25 +18,103 @@ We plan on releasing monthly updates with new features - check the [changelog](h
|
||||
To help with testing app updates before they're released, you can:
|
||||
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P)
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://mattermost-fastlane.herokuapp.com/)
|
||||
2. Install the `Mattermost Beta` app
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
- Device information
|
||||
- Repro steps
|
||||
- Observed behavior (including screenshot / video when possible)
|
||||
- Expected behavior
|
||||
- Device information
|
||||
- Repro steps
|
||||
- Observed behavior (including screenshot / video when possible)
|
||||
- Expected behavior
|
||||
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
|
||||
|
||||
### Contribute Code
|
||||
|
||||
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted]
|
||||
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-mobile/issues) for issues marked as [Help Wanted]
|
||||
2. Comment to let people know you’re working on it
|
||||
3. Follow [these instructions](https://developers.mattermost.com/contribute/mobile/developer-setup/) to set up your developer environment
|
||||
3. Follow [these instructions](https://docs.mattermost.com/developer/mobile-developer-setup.html) to set up your developer environment
|
||||
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
|
||||
|
||||
# Installing Dependencies
|
||||
|
||||
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
|
||||
|
||||
# Detailed configuration:
|
||||
|
||||
## Mac
|
||||
|
||||
- General requirements
|
||||
|
||||
- XCode 8.3
|
||||
- Install required packages using homebrew:
|
||||
```bash
|
||||
$ brew install watchman
|
||||
$ brew install yarn
|
||||
```
|
||||
|
||||
- Clone repository and configure:
|
||||
```bash
|
||||
$ git clone git@github.com:mattermost/mattermost-mobile.git
|
||||
$ cd mattermost-mobile
|
||||
$ npm install
|
||||
$ npm install -g react-native-cli
|
||||
```
|
||||
|
||||
- Run application
|
||||
```bash
|
||||
$ make run
|
||||
```
|
||||
|
||||
- Stop the packager server
|
||||
```bash
|
||||
$ make stop
|
||||
```
|
||||
## Linux:
|
||||
|
||||
- General requiriments:
|
||||
|
||||
- JDK 7 or greater
|
||||
- Android SDK
|
||||
- Virtualbox
|
||||
- An Android emulator: Genymotion or Android emulator. If using genymotion ensure that it uses existing adb tools (Settings: "Use custom Android SDK Tools")
|
||||
- Install watchman (do this globally):
|
||||
```bash
|
||||
$ git clone https://github.com/facebook/watchman.git
|
||||
$ cd watchman
|
||||
$ git checkout master
|
||||
$ ./autogen.sh
|
||||
$ ./configure
|
||||
$ make
|
||||
$ sudo make install
|
||||
```
|
||||
Configure your kernel to accept a lot of file watches, using a command like:
|
||||
```bash
|
||||
$ sudo sysctl -w fs.inotify.max_user_watches=1048576
|
||||
```
|
||||
|
||||
- Clone repository and configure:
|
||||
```bash
|
||||
$ git clone git@github.com:mattermost/mattermost-mobile.git
|
||||
$ cd mattermost-mobile
|
||||
$ npm install
|
||||
$ npm install -g react-native-cli
|
||||
```
|
||||
|
||||
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
|
||||
`{
|
||||
"DefaultServerUrl": "https://pre-release.mattermost.com"
|
||||
}`
|
||||
|
||||
To use a local Mattermost server you will need to configure the "DefaultServerUrl" depending on the emulator you will use:
|
||||
* IOs: "DefaultServerUrl": "http://localhost:8065"
|
||||
* Android: "DefaultServerUrl": "http://10.0.2.2:3000"
|
||||
* Genymotion: "DefaultServerUrl": "http://10.0.3.2:8065"
|
||||
|
||||
- Run application
|
||||
- Start emulator
|
||||
- Start react packager: `$ react-native start`
|
||||
- Run in emulator: `$ react-native run-android`
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
@@ -45,13 +122,13 @@ To help with testing app updates before they're released, you can:
|
||||
|
||||
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
|
||||
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?
|
||||
### Can I connect to multiple Mattermost servers using the mobile apps?**
|
||||
|
||||
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
|
||||
|
||||
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
|
||||
|
||||
### Will there be second generation apps available for tablets?
|
||||
### Will there be second generation apps available for tablets?**
|
||||
|
||||
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!
|
||||
|
||||
@@ -72,3 +149,11 @@ If your app is working properly, you should see a grey “Connecting…” bar t
|
||||
If you are seeing this message all the time, and your internet connection seems fine:
|
||||
|
||||
Ask your server administrator if the server uses NGINX or another webserver as a reverse proxy. If so, they should check that it is configured correctly for [supporting the websocket connection for APIv4 endpoints](https://docs.mattermost.com/install/install-ubuntu-1604.html#configuring-nginx-as-a-proxy-for-mattermost-server).
|
||||
|
||||
# Issues building app for own device using make build-*
|
||||
|
||||
That command is an internal pipeline command for mattermost mobile to publish the mobile apps to ````Apple App Store```` and ````Google Play Store````. All ````make build-*```` commands should be avoided for this reason.
|
||||
|
||||
To build the modified react native client use the instructions for [Running on Device](http://facebook.github.io/react-native/docs/running-on-device.html) from the [React Native Guide](https://facebook.github.io/react-native/docs/getting-started.html).
|
||||
|
||||
|
||||
|
||||
@@ -45,13 +45,13 @@ android_library(
|
||||
|
||||
android_build_config(
|
||||
name = "build_config",
|
||||
package = "com.mattermost.rnbeta",
|
||||
package = "com.mattermost-mobile",
|
||||
)
|
||||
|
||||
android_resource(
|
||||
name = "res",
|
||||
package = "com.mattermost.rnbeta",
|
||||
res = "src/main/res",
|
||||
name = "res",
|
||||
res = "src/main/res",
|
||||
package = "com.mattermost.rnbeta",
|
||||
)
|
||||
|
||||
android_binary(
|
||||
|
||||
@@ -73,20 +73,13 @@ import com.android.build.OutputFile
|
||||
*/
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js",
|
||||
bundleCommand: "ram-bundle",
|
||||
bundleConfig: "metro.config.js"
|
||||
entryFile: "index.js"
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
|
||||
|
||||
if (System.getenv("SENTRY_ENABLED") == "true") {
|
||||
project.ext.sentryCli = [
|
||||
logLevel: "error",
|
||||
flavorAware: false
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
|
||||
}
|
||||
|
||||
@@ -106,27 +99,18 @@ def enableSeparateBuildPerCPUArchitecture = false
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/libjsc.so'
|
||||
pickFirst '**/libc++_shared.so'
|
||||
}
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 204
|
||||
versionName "1.20.2"
|
||||
multiDexEnabled = true
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 83
|
||||
versionName "1.5.3"
|
||||
multiDexEnabled true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -145,7 +129,7 @@ android {
|
||||
reset()
|
||||
enable enableSeparateBuildPerCPUArchitecture
|
||||
universalApk false // If true, also generate a universal APK
|
||||
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
include "armeabi-v7a", "x86"
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
@@ -161,7 +145,6 @@ android {
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
signingConfig null
|
||||
matchingFallbacks = ['debug', 'release']
|
||||
}
|
||||
}
|
||||
// applicationVariants are e.g. debug, release
|
||||
@@ -169,7 +152,7 @@ android {
|
||||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
@@ -177,78 +160,41 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://maven.google.com'
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Make sure to put android-jsc at the top
|
||||
implementation "org.webkit:android-jsc:r241213"
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation 'com.android.support:percent:28.0.0'
|
||||
implementation "com.google.firebase:firebase-messaging:17.3.0"
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation project(':react-native-document-picker')
|
||||
implementation project(':react-native-keychain')
|
||||
implementation project(':react-native-doc-viewer')
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-navigation')
|
||||
implementation project(':react-native-image-picker')
|
||||
implementation project(':react-native-bottom-sheet')
|
||||
implementation project(':react-native-device-info')
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation project(':react-native-cookies')
|
||||
implementation project(':react-native-linear-gradient')
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':react-native-svg')
|
||||
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')
|
||||
compile project(':react-native-doc-viewer')
|
||||
compile project(':react-native-video')
|
||||
compile fileTree(dir: "libs", include: ["*.jar"])
|
||||
compile "com.android.support:appcompat-v7:25.0.1"
|
||||
compile 'com.android.support:percent:25.3.1'
|
||||
compile "com.facebook.react:react-native:+" // From node_modules
|
||||
compile project(':react-native-navigation')
|
||||
compile project(':react-native-image-picker')
|
||||
compile project(':react-native-orientation')
|
||||
compile project(':react-native-bottom-sheet')
|
||||
compile ('com.google.android.gms:play-services-gcm:9.4.0') {
|
||||
force = true;
|
||||
}
|
||||
compile project(':react-native-device-info')
|
||||
compile project(':reactnativenotifications')
|
||||
compile project(':react-native-cookies')
|
||||
compile project(':react-native-linear-gradient')
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-svg')
|
||||
compile project(':react-native-local-auth')
|
||||
compile project(':jail-monkey')
|
||||
compile project(':react-native-youtube')
|
||||
compile project(':react-native-sentry')
|
||||
compile project(':react-native-exception-handler')
|
||||
compile project(':react-native-fetch-blob')
|
||||
|
||||
// For animated GIF support
|
||||
implementation 'com.facebook.fresco:fresco:1.10.0'
|
||||
implementation 'com.facebook.fresco:animated-gif:1.10.0'
|
||||
compile 'com.facebook.fresco:animated-base-support:1.3.0'
|
||||
// For WebP support, including animated WebP
|
||||
implementation 'com.facebook.fresco:animated-webp:1.10.0'
|
||||
implementation 'com.facebook.fresco:webpsupport:1.10.0'
|
||||
compile 'com.facebook.fresco:animated-gif:1.3.0'
|
||||
compile 'com.facebook.fresco:animated-webp:1.3.0'
|
||||
compile 'com.facebook.fresco:webpsupport:1.3.0'
|
||||
}
|
||||
|
||||
// Run this once to be able to run the application with BUCK
|
||||
@@ -257,5 +203,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -57,7 +57,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -88,7 +88,7 @@
|
||||
],
|
||||
"services": {
|
||||
"analytics_service": {
|
||||
"status": 2
|
||||
"status": 1
|
||||
},
|
||||
"appinvite_service": {
|
||||
"status": 1,
|
||||
@@ -101,4 +101,4 @@
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
53
android/app/proguard-rules.pro
vendored
@@ -15,3 +15,56 @@
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Disabling obfuscation is useful if you collect stack traces from production crashes
|
||||
# (unless you are using a system that supports de-obfuscate the stack traces).
|
||||
-dontobfuscate
|
||||
|
||||
# React Native
|
||||
|
||||
# Keep our interfaces so they can be used by other ProGuard rules.
|
||||
# See http://sourceforge.net/p/proguard/bugs/466/
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
|
||||
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
|
||||
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
|
||||
|
||||
# Do not strip any method/class that is annotated with @DoNotStrip
|
||||
-keep @com.facebook.proguard.annotations.DoNotStrip class *
|
||||
-keep @com.facebook.common.internal.DoNotStrip class *
|
||||
-keepclassmembers class * {
|
||||
@com.facebook.proguard.annotations.DoNotStrip *;
|
||||
@com.facebook.common.internal.DoNotStrip *;
|
||||
}
|
||||
|
||||
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
|
||||
void set*(***);
|
||||
*** get*();
|
||||
}
|
||||
|
||||
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
|
||||
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
|
||||
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
|
||||
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
|
||||
|
||||
-dontwarn com.facebook.react.**
|
||||
|
||||
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
|
||||
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
|
||||
-dontwarn android.text.StaticLayout
|
||||
|
||||
# okhttp
|
||||
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class okhttp3.** { *; }
|
||||
-keep interface okhttp3.** { *; }
|
||||
-dontwarn okhttp3.**
|
||||
|
||||
# okio
|
||||
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-dontwarn okio.**
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
||||
@@ -1,34 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost.rnbeta">
|
||||
package="com.mattermost.rnbeta"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<permission
|
||||
android:name="${applicationId}.permission.C2D_MESSAGE"
|
||||
android:protectionLevel="signature" />
|
||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.SEND" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="22" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:allowBackup="false"
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme"
|
||||
android:installLocation="auto"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
<meta-data android:name="android.content.APP_RESTRICTIONS"
|
||||
android:resource="@xml/app_restrictions" />
|
||||
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
|
||||
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -39,27 +50,12 @@
|
||||
<service android:name=".NotificationDismissService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name=".NotificationReplyBroadcastReceiver"
|
||||
<service android:name=".NotificationReplyService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:resizeableActivity="true"/>
|
||||
<activity
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/AppTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<!-- for sharing-->
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.mattermost.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputType;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
public class CustomTextInput extends ReactEditText {
|
||||
private boolean autoScroll = false;
|
||||
|
||||
public CustomTextInput(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private boolean isMultiline() {
|
||||
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLayoutRequested() {
|
||||
if (isMultiline() && !autoScroll) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setAutoScroll(boolean autoScroll) {
|
||||
this.autoScroll = autoScroll;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.mattermost.components;
|
||||
|
||||
import android.text.InputType;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||
import com.facebook.react.uimanager.PixelUtil;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.uimanager.ViewDefaults;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
|
||||
public class CustomTextInputManager extends ReactTextInputManager {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "CustomTextInput";
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomTextInput createViewInstance(ThemedReactContext context) {
|
||||
CustomTextInput editText = new CustomTextInput(context);
|
||||
int inputType = editText.getInputType();
|
||||
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
|
||||
editText.setReturnKeyType("done");
|
||||
editText.setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
|
||||
return editText;
|
||||
}
|
||||
|
||||
@ReactProp(name = "autoScroll", defaultBoolean = false)
|
||||
public void setAutoScroll(CustomTextInput view, boolean autoScroll) {
|
||||
view.setAutoScroll(autoScroll);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.mattermost.react_native_interface;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.modules.storage.ReactDatabaseSupplier;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* AsyncStorageHelper: Class that accesses React Native AsyncStorage Database synchronously
|
||||
*/
|
||||
public class AsyncStorageHelper {
|
||||
|
||||
// Static variables from: com.facebook.react.modules.storage.ReactDatabaseSupplier
|
||||
static final String TABLE_CATALYST = "catalystLocalStorage";
|
||||
static final String KEY_COLUMN = "key";
|
||||
static final String VALUE_COLUMN = "value";
|
||||
|
||||
|
||||
private static final int MAX_SQL_KEYS = 999;
|
||||
|
||||
Context mReactContext = null;
|
||||
|
||||
public AsyncStorageHelper(Context mReactContext) {
|
||||
this.mReactContext = mReactContext;
|
||||
}
|
||||
|
||||
public HashMap<String, String> multiGet(ReadableArray keys) {
|
||||
HashMap<String, String> results = new HashMap<>(keys.size());
|
||||
|
||||
HashSet<String> keysRemaining = new HashSet<>();
|
||||
String[] columns = {KEY_COLUMN, VALUE_COLUMN};
|
||||
ReactDatabaseSupplier reactDatabaseSupplier = ReactDatabaseSupplier.getInstance(this.mReactContext);
|
||||
for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) {
|
||||
int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS);
|
||||
Cursor cursor = reactDatabaseSupplier.get().query(
|
||||
TABLE_CATALYST,
|
||||
columns,
|
||||
buildKeySelection(keyCount),
|
||||
buildKeySelectionArgs(keys, keyStart, keyCount),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
keysRemaining.clear();
|
||||
|
||||
try {
|
||||
if (cursor.getCount() != keys.size()) {
|
||||
// some keys have not been found - insert them with null into the final array
|
||||
for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) {
|
||||
keysRemaining.add(keys.getString(keyIndex));
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
results.put(cursor.getString(0), cursor.getString(1));
|
||||
keysRemaining.remove(cursor.getString(0));
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return new HashMap<>(1);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
for (String key : keysRemaining) {
|
||||
results.put(key, null);
|
||||
}
|
||||
keysRemaining.clear();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static String buildKeySelection(int selectionCount) {
|
||||
String[] list = new String[selectionCount];
|
||||
Arrays.fill(list, "?");
|
||||
return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
|
||||
}
|
||||
|
||||
private static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) {
|
||||
String[] selectionArgs = new String[count];
|
||||
for (int keyIndex = 0; keyIndex < count; keyIndex++) {
|
||||
selectionArgs[keyIndex] = keys.getString(start + keyIndex);
|
||||
}
|
||||
return selectionArgs;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.mattermost.react_native_interface;
|
||||
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* KeysReadableArray: Helper class that abstracts boilerplate
|
||||
*/
|
||||
public class KeysReadableArray implements ReadableArray {
|
||||
@Override
|
||||
public int size() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(int index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableArray getArray(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableMap getMap(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dynamic getDynamic(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadableType getType(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Object> toArrayList() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.mattermost.react_native_interface;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
/**
|
||||
* ResolvePromise: Helper class that abstracts boilerplate
|
||||
*/
|
||||
public class ResolvePromise implements Promise {
|
||||
@Override
|
||||
public void resolve(@javax.annotation.Nullable Object value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String message) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable reason) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Person;
|
||||
import android.app.Person.Builder;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
@@ -19,14 +13,15 @@ import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Build;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.provider.Settings.System;
|
||||
import android.util.Log;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
@@ -35,9 +30,12 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
@@ -48,46 +46,31 @@ public class CustomPushNotification extends PushNotification {
|
||||
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;
|
||||
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static void clearNotification(Context mContext, int notificationId, String channelId) {
|
||||
public static void clearNotification(int notificationId, String channelId) {
|
||||
if (notificationId != -1) {
|
||||
Object objCount = channelIdToNotificationCount.get(channelId);
|
||||
Integer count = -1;
|
||||
|
||||
if (objCount != null) {
|
||||
count = (Integer)objCount;
|
||||
}
|
||||
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
|
||||
if (mContext != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (context != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
|
||||
if (count != -1) {
|
||||
int total = CustomPushNotification.badgeCount - count;
|
||||
int badgeCount = total < 0 ? 0 : total;
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), badgeCount);
|
||||
CustomPushNotification.badgeCount = badgeCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAllNotifications(Context mContext) {
|
||||
channelIdToNotificationCount.clear();
|
||||
channelIdToNotification.clear();
|
||||
if (mContext != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancelAll();
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), 0);
|
||||
public static void clearNotification(Context mContext, int notificationId, String channelId) {
|
||||
if (notificationId != -1) {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
if (mContext != null) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,13 +79,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String type = data.getString("type");
|
||||
final String ackId = data.getString("ack_id");
|
||||
int notificationId = MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (ackId != null) {
|
||||
notificationReceiptDelivery(ackId, type);
|
||||
}
|
||||
|
||||
if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
Object objCount = channelIdToNotificationCount.get(channelId);
|
||||
@@ -120,12 +97,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
list = Collections.synchronizedList((List)bundleArray);
|
||||
}
|
||||
synchronized (list) {
|
||||
if (!"clear".equals(type)) {
|
||||
String senderName = getSenderName(data.getString("sender_name"), data.getString("channel_name"), data.getString("message"));
|
||||
data.putLong("time", new Date().getTime());
|
||||
data.putString("sender_name", senderName);
|
||||
data.putString("sender_id", data.getString("sender_id"));
|
||||
}
|
||||
list.add(0, data);
|
||||
channelIdToNotification.put(channelId, list);
|
||||
}
|
||||
@@ -147,6 +118,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
digestNotification();
|
||||
clearAllNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -170,53 +142,26 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
// First, get a builder initialized with defaults from the core class.
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
|
||||
// If Android Oreo or above we need to register a channel
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
String CHANNEL_ID = "channel_01";
|
||||
String CHANNEL_NAME = "Mattermost notifications";
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.setShowBadge(true);
|
||||
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
notification.setChannelId(CHANNEL_ID);
|
||||
}
|
||||
|
||||
Bundle bundle = mNotificationProps.asBundle();
|
||||
|
||||
String version = bundle.getString("version");
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String channelName = bundle.getString("channel_name");
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String badge = bundle.getString("badge");
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
String title = null;
|
||||
if (version != null && version.equals("v2")) {
|
||||
title = channelName;
|
||||
} else {
|
||||
title = bundle.getString("title");
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(title)) {
|
||||
String title = bundle.getString("title");
|
||||
if (title == null) {
|
||||
ApplicationInfo appInfo = mContext.getApplicationInfo();
|
||||
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
|
||||
}
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
String message = bundle.getString("message");
|
||||
String subText = bundle.getString("subText");
|
||||
String numberString = bundle.getString("badge");
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
String largeIcon = bundle.getString("largeIcon");
|
||||
|
||||
Bundle b = bundle.getBundle("userInfo");
|
||||
if (b == null) {
|
||||
b = new Bundle();
|
||||
if (b != null) {
|
||||
notification.addExtras(b);
|
||||
}
|
||||
b.putString("channel_id", channelId);
|
||||
notification.addExtras(b);
|
||||
|
||||
int smallIconResId;
|
||||
int largeIconResId;
|
||||
@@ -241,80 +186,49 @@ public class CustomPushNotification extends PushNotification {
|
||||
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
|
||||
}
|
||||
|
||||
if (badge != null) {
|
||||
int badgeCount = Integer.parseInt(badge);
|
||||
CustomPushNotification.badgeCount = badgeCount;
|
||||
notification.setNumber(badgeCount);
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
if (numberString != null) {
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
if (android.text.TextUtils.isEmpty(senderName)) {
|
||||
senderName = getSenderName(senderName, channelName, bundle.getString("message"));
|
||||
}
|
||||
|
||||
String personId = senderId;
|
||||
if (!android.text.TextUtils.isEmpty(channelName)) {
|
||||
personId = channelId;
|
||||
}
|
||||
|
||||
Notification.MessagingStyle messagingStyle;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle = new Notification.MessagingStyle("");
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(senderId)
|
||||
.setName("")
|
||||
.build();
|
||||
messagingStyle = new Notification.MessagingStyle(sender);
|
||||
}
|
||||
|
||||
if (title != null && (!title.startsWith("@") || channelName != senderName)) {
|
||||
messagingStyle
|
||||
.setConversationTitle(title);
|
||||
}
|
||||
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
list.add(bundle);
|
||||
}
|
||||
|
||||
int listCount = list.size() - 1;
|
||||
for (int i = listCount; i >= 0; i--) {
|
||||
Bundle data = list.get(i);
|
||||
String message = data.getString("message");
|
||||
String previousPersonName = getSenderName(data.getString("sender_name"), channelName, message);
|
||||
String previousPersonId = data.getString("sender_id");
|
||||
|
||||
if (title == null || !android.text.TextUtils.isEmpty(previousPersonName)) {
|
||||
message = removeSenderFromMessage(previousPersonName, channelName, message);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
messagingStyle.addMessage(message, data.getLong("time"), previousPersonName);
|
||||
} else {
|
||||
Person sender = new Person.Builder()
|
||||
.setKey(previousPersonId)
|
||||
.setName(previousPersonName)
|
||||
.build();
|
||||
messagingStyle.addMessage(message, data.getLong("time"), sender);
|
||||
}
|
||||
}
|
||||
int numMessages = getMessageCountInChannel(channelId);
|
||||
|
||||
notification
|
||||
.setContentIntent(intent)
|
||||
.setGroupSummary(true)
|
||||
.setStyle(messagingStyle)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.setVisibility(Notification.VISIBILITY_PRIVATE)
|
||||
.setPriority(Notification.PRIORITY_HIGH)
|
||||
.setAutoCancel(true);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notification.setBadgeIconType(Notification.BADGE_ICON_SMALL);
|
||||
if (numMessages == 1) {
|
||||
notification
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(new Notification.BigTextStyle()
|
||||
.bigText(message));
|
||||
} else {
|
||||
String summaryTitle = String.format("%s (%d)", title, numMessages);
|
||||
|
||||
Notification.InboxStyle style = new Notification.InboxStyle();
|
||||
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
|
||||
List<Bundle> list;
|
||||
if (bundleArray != null) {
|
||||
list = new ArrayList<Bundle>(bundleArray);
|
||||
} else {
|
||||
list = new ArrayList<Bundle>();
|
||||
}
|
||||
|
||||
for (Bundle data : list) {
|
||||
String msg = data.getString("message");
|
||||
if (msg != message) {
|
||||
style.addLine(data.getString("message"));
|
||||
}
|
||||
}
|
||||
|
||||
style.setBigContentTitle(message)
|
||||
.setSummaryText(String.format("+%d more", (numMessages - 1)));
|
||||
notification.setStyle(style)
|
||||
.setContentTitle(summaryTitle);
|
||||
}
|
||||
|
||||
// Let's add a delete intent when the notification is dismissed
|
||||
@@ -328,11 +242,11 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
|
||||
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
||||
Intent replyIntent = new Intent(mContext, NotificationReplyService.class);
|
||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
|
||||
.setLabel("Reply")
|
||||
@@ -354,6 +268,10 @@ public class CustomPushNotification extends PushNotification {
|
||||
notification.setLargeIcon(largeIconBitmap);
|
||||
}
|
||||
|
||||
if (subText != null) {
|
||||
notification.setSubText(subText);
|
||||
}
|
||||
|
||||
String soundUri = notificationPreferences.getNotificationSound();
|
||||
if (soundUri != null) {
|
||||
if (soundUri != "none") {
|
||||
@@ -393,35 +311,15 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
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 numberString = data.getString("badge");
|
||||
if (numberString != null) {
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
String name = message.split(":")[0];
|
||||
if (name != message) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return " ";
|
||||
}
|
||||
|
||||
private String removeSenderFromMessage(String senderName, String channelName, String message) {
|
||||
String sender = String.format("%s", getSenderName(senderName, channelName, message));
|
||||
return message.replaceFirst(sender, "").replaceFirst(": ", "").trim();
|
||||
}
|
||||
|
||||
private void notificationReceiptDelivery(String ackId, String type) {
|
||||
ReceiptDelivery.send(context, ackId, type);
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
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 {
|
||||
final protected Context mContext;
|
||||
final protected AppLaunchHelper mAppLaunchHelper;
|
||||
|
||||
protected CustomPushNotificationDrawer(Context context, AppLaunchHelper appLaunchHelper) {
|
||||
super(context, appLaunchHelper);
|
||||
mContext = context;
|
||||
mAppLaunchHelper = appLaunchHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppVisible() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationOpened() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelAllLocalNotifications() {
|
||||
CustomPushNotification.clearAllNotifications(mContext);
|
||||
cancelAllScheduledNotifications();
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.mattermost.react_native_interface.AsyncStorageHelper;
|
||||
import com.mattermost.react_native_interface.KeysReadableArray;
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class InitializationModule extends ReactContextBaseJavaModule {
|
||||
|
||||
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
|
||||
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
|
||||
static final String APP_BACKGROUND = "APP_BACKGROUND";
|
||||
|
||||
private final Application mApplication;
|
||||
|
||||
public InitializationModule(Application application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Initialization";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
Map<String, Object> constants = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Package all native module variables in constants
|
||||
* in order to avoid the native bridge
|
||||
*
|
||||
* KeyStore:
|
||||
* credentialsExist
|
||||
* deviceToken
|
||||
* currentUserId
|
||||
* token
|
||||
* url
|
||||
*
|
||||
* AsyncStorage:
|
||||
* toolbarBackground
|
||||
* toolbarTextColor
|
||||
* appBackground
|
||||
*
|
||||
* Miscellaneous:
|
||||
* MattermostManaged.Config
|
||||
* replyFromPushNotification
|
||||
*/
|
||||
|
||||
MainApplication app = (MainApplication) mApplication;
|
||||
final Boolean[] credentialsExist = {false};
|
||||
final WritableMap[] credentials = {null};
|
||||
final Object[] config = {null};
|
||||
|
||||
// Get KeyStore credentials
|
||||
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
|
||||
module.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
credentialsExist[0] = false;
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
credentialsExist[0] = true;
|
||||
credentials[0] = map;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get managedConfig from MattermostManagedModule
|
||||
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
WritableNativeMap nativeMap = (WritableNativeMap) value;
|
||||
config[0] = value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get AsyncStorage key/values
|
||||
final ArrayList<String> keys = new ArrayList<String>(5);
|
||||
keys.add(TOOLBAR_BACKGROUND);
|
||||
keys.add(TOOLBAR_TEXT_COLOR);
|
||||
keys.add(APP_BACKGROUND);
|
||||
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return keys.get(index);
|
||||
}
|
||||
};
|
||||
|
||||
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
|
||||
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
|
||||
|
||||
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
|
||||
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
|
||||
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
|
||||
|
||||
if (toolbarBackground != null
|
||||
&& toolbarTextColor != null
|
||||
&& appBackground != null) {
|
||||
|
||||
constants.put("themesExist", true);
|
||||
constants.put("toolbarBackground", toolbarBackground);
|
||||
constants.put("toolbarTextColor", toolbarTextColor);
|
||||
constants.put("appBackground", appBackground);
|
||||
} else {
|
||||
constants.put("themesExist", false);
|
||||
}
|
||||
|
||||
|
||||
if (credentialsExist[0]) {
|
||||
constants.put("credentialsExist", true);
|
||||
constants.put("credentials", credentials[0]);
|
||||
} else {
|
||||
constants.put("credentialsExist", false);
|
||||
}
|
||||
|
||||
constants.put("managedConfig", config[0]);
|
||||
constants.put("replyFromPushNotification", app.replyFromPushNotification);
|
||||
app.replyFromPushNotification = false;
|
||||
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class InitializationPackage implements ReactPackage {
|
||||
|
||||
private final Application mApplication;
|
||||
|
||||
public InitializationPackage(Application application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,10 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import com.reactnativenavigation.controllers.SplashActivity;
|
||||
|
||||
public class MainActivity extends SplashActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
/**
|
||||
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
|
||||
* 1. Open app from launcher/appDrawer
|
||||
* 2. Go home
|
||||
* 3. Send notification and open
|
||||
* 4. It creates a new Activity and Destroys the old
|
||||
* 5. Causing an unnecessary app restart
|
||||
* 6. This solution short-circuits the restart
|
||||
*/
|
||||
if (!isTaskRoot()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSplashLayout() {
|
||||
return R.layout.launch_screen;
|
||||
}
|
||||
@Override
|
||||
public int getSplashLayout() {
|
||||
return R.layout.launch_screen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.mattermost.share.SharePackage;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
|
||||
import com.oblador.keychain.KeychainPackage;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.reactlibrary.RNReactNativeDocViewerPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
import com.horcrux.svg.SvgPackage;
|
||||
@@ -22,12 +15,10 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
import com.gantix.JailMonkey.JailMonkeyPackage;
|
||||
import io.tradle.react.LocalAuthPackage;
|
||||
import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
|
||||
import com.reactnativecommunity.netinfo.NetInfoPackage;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
|
||||
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.imagepicker.ImagePickerPackage;
|
||||
@@ -36,39 +27,20 @@ import com.learnium.RNDeviceInfo.RNDeviceInfo;
|
||||
import com.psykar.cookiemanager.CookieManagerPackage;
|
||||
import com.oblador.vectoricons.VectorIconsPackage;
|
||||
import com.BV.LinearGradient.LinearGradientPackage;
|
||||
import com.github.yamill.orientation.OrientationPackage;
|
||||
import com.reactnativenavigation.NavigationApplication;
|
||||
import com.wix.reactnativenotifications.RNNotificationsPackage;
|
||||
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
|
||||
import com.wix.reactnativenotifications.core.notification.IPushNotification;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
|
||||
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactMarker;
|
||||
import com.facebook.react.bridge.ReactMarkerConstants;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import android.support.annotation.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication {
|
||||
public NotificationsLifecycleFacade notificationsLifecycleFacade;
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
public Boolean replyFromPushNotification = false;
|
||||
|
||||
public long APP_START_TIME;
|
||||
|
||||
public long RELOAD;
|
||||
public long CONTENT_APPEARED;
|
||||
|
||||
public long PROCESS_PACKAGES_START;
|
||||
public long PROCESS_PACKAGES_END;
|
||||
|
||||
@Override
|
||||
public boolean isDebug() {
|
||||
@@ -88,24 +60,17 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new VectorIconsPackage(),
|
||||
new SvgPackage(),
|
||||
new LinearGradientPackage(),
|
||||
new OrientationPackage(),
|
||||
new RNNotificationsPackage(this),
|
||||
new LocalAuthPackage(),
|
||||
new JailMonkeyPackage(),
|
||||
new RNFetchBlobPackage(),
|
||||
new MattermostPackage(this),
|
||||
new RNSentryPackage(),
|
||||
new RNSentryPackage(this),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube(),
|
||||
new ReactVideoPackage(),
|
||||
new RNReactNativeDocViewerPackage(),
|
||||
new ReactNativeDocumentPicker(),
|
||||
new SharePackage(this),
|
||||
new KeychainPackage(),
|
||||
new InitializationPackage(this),
|
||||
new AsyncStoragePackage(),
|
||||
new NetInfoPackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new RNGestureHandlerPackage()
|
||||
new RNReactNativeDocViewerPackage()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,28 +83,12 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
|
||||
RealPathUtil.deleteTempFiles(tempFolder);
|
||||
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
|
||||
|
||||
// Create an object of the custom facade impl
|
||||
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
|
||||
// Attach it to react-native-navigation
|
||||
setActivityCallbacks(notificationsLifecycleFacade);
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
|
||||
// Uncomment to listen to react markers for build that has telemetry enabled
|
||||
// addReactMarkerListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearHostOnActivityDestroy(Activity activity) {
|
||||
// This solves the issue where the splash screen does not go away
|
||||
// after the app is killed by the OS cause of memory or a long time in the background
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,41 +101,4 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new JsIOHelper()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
|
||||
}
|
||||
|
||||
private void addReactMarkerListener() {
|
||||
ReactMarker.addListener(new ReactMarker.MarkerListener() {
|
||||
@Override
|
||||
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
|
||||
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
|
||||
APP_START_TIME = System.currentTimeMillis();
|
||||
RELOAD = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
|
||||
PROCESS_PACKAGES_START = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
|
||||
PROCESS_PACKAGES_END = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
|
||||
CONTENT_APPEARED = System.currentTimeMillis();
|
||||
ReactContext ctx = getReactGateway().getReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
map.putDouble("appReload", RELOAD);
|
||||
map.putDouble("appContentAppeared", CONTENT_APPEARED);
|
||||
|
||||
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
|
||||
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
|
||||
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("nativeMetrics", map);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
@@ -13,7 +11,6 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static MattermostManagedModule instance;
|
||||
@@ -59,37 +56,10 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
promise.resolve(result);
|
||||
} else {
|
||||
promise.resolve(Arguments.createMap());
|
||||
throw new Exception("The MDM vendor has not sent any Managed configuration");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
promise.resolve(Arguments.createMap());
|
||||
promise.reject("no managed configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
// Close the current activity and open the security settings.
|
||||
public void goToSecuritySettings() {
|
||||
getReactApplicationContext().startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
|
||||
getCurrentActivity().finish();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isRunningInSplitView(final Promise promise) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
Activity current = getCurrentActivity();
|
||||
if (current != null) {
|
||||
result.putBoolean("isSplitView", current.isInMultiWindowMode());
|
||||
} else {
|
||||
result.putBoolean("isSplitView", false);
|
||||
}
|
||||
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void quitApp() {
|
||||
getCurrentActivity().finish();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
|
||||
import com.mattermost.components.CustomTextInputManager;
|
||||
|
||||
public class MattermostPackage implements ReactPackage {
|
||||
private final MainApplication mApplication;
|
||||
|
||||
@@ -21,13 +23,14 @@ public class MattermostPackage implements ReactPackage {
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(
|
||||
MattermostManagedModule.getInstance(reactContext),
|
||||
NotificationPreferencesModule.getInstance(mApplication, reactContext),
|
||||
new RNTextInputResetModule(reactContext)
|
||||
NotificationPreferencesModule.getInstance(mApplication, reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList();
|
||||
return Arrays.<ViewManager>asList(
|
||||
new CustomTextInputManager()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
public class NotificationDismissService extends IntentService {
|
||||
private Context mContext;
|
||||
public NotificationDismissService() {
|
||||
super("notificationDismissService");
|
||||
super("notificationDismissService");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.media.Ringtone;
|
||||
import android.media.RingtoneManager;
|
||||
import android.os.Bundle;
|
||||
import android.net.Uri;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
@@ -108,29 +105,4 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
public void setShouldBlink(boolean blink) {
|
||||
mNotificationPreference.setShouldBlink(blink);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getDeliveredNotifications(final Promise promise) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
|
||||
WritableArray result = Arguments.createArray();
|
||||
for (StatusBarNotification sbn:statusBarNotifications) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
int identifier = sbn.getId();
|
||||
String channelId = bundle.getString("channel_id");
|
||||
map.putInt("identifier", identifier);
|
||||
map.putString("channel_id", channelId);
|
||||
result.pushMap(map);
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeDeliveredNotifications(int identifier, String channelId) {
|
||||
Context context = mApplication.getApplicationContext();
|
||||
CustomPushNotification.clearNotification(context, identifier, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
private Context mContext;
|
||||
private Bundle bundle;
|
||||
private NotificationManager notificationManager;
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
mContext = context;
|
||||
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
final CharSequence message = getReplyMessage(intent);
|
||||
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
|
||||
|
||||
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
String channelId = bundle.getString("channel_id");
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
String[] credentials = map.getString("password").split(",[ ]*");
|
||||
String token = null;
|
||||
String serverUrl = null;
|
||||
if (credentials.length == 2) {
|
||||
token = credentials[0];
|
||||
serverUrl = credentials[1];
|
||||
|
||||
}
|
||||
|
||||
Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token));
|
||||
replyToMessage(serverUrl, token, notificationId, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String rootId = bundle.getString("post_id");
|
||||
|
||||
if (token == null || serverUrl == null) {
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
return;
|
||||
}
|
||||
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
String json = buildReplyPost(channelId, rootId, message.toString());
|
||||
Log.i("ReactNative", String.format("JSON STRING %s", json));
|
||||
RequestBody body = RequestBody.create(JSON, json);
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.url(String.format("%s/api/v4/posts", serverUrl.replaceAll("/$", "")))
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
client.newCall(request).enqueue(new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
Log.i("ReactNative", String.format("Reply with message %s FAILED exception %s", message, e.getMessage()));
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, final Response response) throws IOException {
|
||||
if (response.isSuccessful()) {
|
||||
onReplySuccess(notificationManager, notificationId, channelId);
|
||||
Log.i("ReactNative", String.format("Reply with message %s", message));
|
||||
} else {
|
||||
Log.i("ReactNative", String.format("Reply with message %s FAILED status %s BODY %s", message, response.code(), response.body().string()));
|
||||
onReplyFailed(notificationManager, notificationId, channelId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String buildReplyPost(String channelId, String rootId, String message) {
|
||||
return "{"
|
||||
+ "\"channel_id\": \"" + channelId + "\","
|
||||
+ "\"message\": \"" + message + "\","
|
||||
+ "\"root_id\": \"" + rootId + "\""
|
||||
+ "}";
|
||||
}
|
||||
|
||||
protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) {
|
||||
String CHANNEL_ID = "Reply job";
|
||||
Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Message failed to send.")
|
||||
.setSmallIcon(smallIconResId)
|
||||
.build();
|
||||
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
notificationManager.notify(notificationId, notification);
|
||||
}
|
||||
|
||||
protected void onReplySuccess(NotificationManager notificationManager, int notificationId, String channelId) {
|
||||
notificationManager.cancel(notificationId);
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
}
|
||||
|
||||
private CharSequence getReplyMessage(Intent intent) {
|
||||
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.HeadlessJsTaskService;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
||||
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
|
||||
public class NotificationReplyService extends HeadlessJsTaskService {
|
||||
private Context mContext;
|
||||
|
||||
@Override
|
||||
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
||||
mContext = getApplicationContext();
|
||||
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
|
||||
CharSequence message = getReplyMessage(intent);
|
||||
|
||||
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
String channelId = bundle.getString("channel_id");
|
||||
bundle.putCharSequence("text", message);
|
||||
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
|
||||
|
||||
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
|
||||
|
||||
Log.i("ReactNative", "Replying service");
|
||||
return new HeadlessJsTaskConfig(
|
||||
"notificationReplied",
|
||||
Arguments.fromBundle(bundle),
|
||||
5000);
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private CharSequence getReplyMessage(Intent intent) {
|
||||
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.ArraySet;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManager.LayoutParams;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
@@ -40,11 +39,12 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
|
||||
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
|
||||
@Override public void onReceive(Context context, Intent intent) {
|
||||
if (context != null) {
|
||||
|
||||
if (mVisibleActivity != null) {
|
||||
// Get the current configuration bundle
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) context
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
(RestrictionsManager) mVisibleActivity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
|
||||
// Check current configuration settings, change your app's UI and
|
||||
@@ -66,28 +66,22 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
|
||||
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
|
||||
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
|
||||
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
|
||||
LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
|
||||
if (managedConfig!= null && managedConfig.size() > 0) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
|
||||
if (activity != null) {
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
switchToVisible(activity);
|
||||
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
if (managedConfig != null && managedConfig.size() > 0 && ctx != null) {
|
||||
|
||||
if (managedConfig != null && managedConfig.size() > 0) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) ctx
|
||||
(RestrictionsManager) activity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
@@ -180,15 +174,13 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void LoadManagedConfig(ReactContext ctx) {
|
||||
if (ctx != null) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) ctx
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
public synchronized void LoadManagedConfig(Activity activity) {
|
||||
RestrictionsManager myRestrictionsMgr =
|
||||
(RestrictionsManager) activity
|
||||
.getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
myRestrictionsMgr = null;
|
||||
}
|
||||
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
|
||||
myRestrictionsMgr = null;
|
||||
}
|
||||
|
||||
public synchronized Bundle getManagedConfig() {
|
||||
@@ -196,10 +188,8 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
LoadManagedConfig(ctx);
|
||||
if (mVisibleActivity != null) {
|
||||
LoadManagedConfig(mVisibleActivity);
|
||||
return managedConfig;
|
||||
}
|
||||
|
||||
@@ -208,11 +198,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
|
||||
public void sendConfigChanged(Bundle config) {
|
||||
Object result = Arguments.fromBundle(config);
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
if (ctx != null) {
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("managedConfigDidChange", result);
|
||||
}
|
||||
getRunningReactContext().
|
||||
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("managedConfigDidChange", result);
|
||||
}
|
||||
|
||||
private boolean equalBundles(Bundle one, Bundle two) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.uimanager.UIManagerModule;
|
||||
import com.facebook.react.uimanager.UIBlock;
|
||||
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
public class RNTextInputResetModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private final ReactApplicationContext reactContext;
|
||||
|
||||
public RNTextInputResetModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RNTextInputReset";
|
||||
}
|
||||
|
||||
// https://github.com/facebook/react-native/pull/12462#issuecomment-298812731
|
||||
@ReactMethod
|
||||
public void resetKeyboardInput(final int reactTagToReset) {
|
||||
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
|
||||
uiManager.addUIBlock(new UIBlock() {
|
||||
@Override
|
||||
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
|
||||
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
|
||||
imm.restartInput(viewToReset);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import java.lang.System;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.oblador.keychain.KeychainModule;
|
||||
|
||||
public class ReceiptDelivery {
|
||||
public static void send (Context context, final String ackId, final String type) {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
|
||||
|
||||
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
return;
|
||||
}
|
||||
|
||||
WritableMap map = (WritableMap) value;
|
||||
if (map != null) {
|
||||
String[] credentials = map.getString("password").split(",[ ]*");
|
||||
String token = null;
|
||||
String serverUrl = null;
|
||||
if (credentials.length == 2) {
|
||||
token = credentials[0];
|
||||
serverUrl = credentials[1];
|
||||
|
||||
}
|
||||
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token));
|
||||
execute(serverUrl, token, ackId, type);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static void execute(String serverUrl, String token, String ackId, String type) {
|
||||
if (token == null || serverUrl == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject json;
|
||||
long receivedAt = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
json = new JSONObject();
|
||||
json.put("id", ackId);
|
||||
json.put("received_at", receivedAt);
|
||||
json.put("platform", "android");
|
||||
json.put("type", type);
|
||||
} catch (JSONException e) {
|
||||
Log.e("ReactNative", "Receipt delivery failed to build json payload");
|
||||
return;
|
||||
}
|
||||
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
RequestBody body = RequestBody.create(JSON, json.toString());
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.header("Content-Type", "application/json")
|
||||
.url(String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")))
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try {
|
||||
client.newCall(request).execute();
|
||||
} catch (Exception e) {
|
||||
Log.e("ReactNative", "Receipt delivery failed to send");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.util.Log;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) {
|
||||
// DownloadsProvider
|
||||
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
if (!TextUtils.isEmpty(id)) {
|
||||
if (id.startsWith("raw:")) {
|
||||
return id.replaceFirst("raw:", "");
|
||||
}
|
||||
try {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else if (isMediaDocument(uri)) {
|
||||
// MediaProvider
|
||||
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] {
|
||||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
// MediaStore (and general)
|
||||
|
||||
if (isGooglePhotosUri(uri)) {
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
|
||||
// Try save to tmp file, and return tmp file path
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
|
||||
File tmpFile;
|
||||
String fileName = null;
|
||||
|
||||
// Try and get the filename from the Uri
|
||||
try {
|
||||
Cursor returnCursor =
|
||||
context.getContentResolver().query(uri, null, null, null, null);
|
||||
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
returnCursor.moveToFirst();
|
||||
fileName = returnCursor.getString(nameIndex);
|
||||
} catch (Exception e) {
|
||||
// just continue to get the filename with the last segment of the path
|
||||
}
|
||||
|
||||
try {
|
||||
if (fileName == null) {
|
||||
fileName = uri.getLastPathSegment().toString().trim();
|
||||
}
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), "mmShare");
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(uri.getPath());
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int index = cursor.getColumnIndexOrThrow(column);
|
||||
return cursor.getString(index);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
public static String getExtension(String uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int dot = uri.lastIndexOf(".");
|
||||
if (dot >= 0) {
|
||||
return uri.substring(dot);
|
||||
} else {
|
||||
// No extension.
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMimeType(File file) {
|
||||
|
||||
String extension = getExtension(file.getName());
|
||||
|
||||
if (extension.length() > 0)
|
||||
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
public static String getMimeType(String filePath) {
|
||||
File file = new File(filePath);
|
||||
return getMimeType(file);
|
||||
}
|
||||
|
||||
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
|
||||
try {
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
} catch (Exception e) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
try {
|
||||
if (dir.isDirectory()) {
|
||||
deleteRecursive(dir);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory())
|
||||
for (File child : fileOrDirectory.listFiles())
|
||||
deleteRecursive(child);
|
||||
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
public class ShareActivity extends ReactActivity {
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
MainApplication app = (MainApplication) this.getApplication();
|
||||
app.sharedExtensionIsOpened = true;
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private final MainApplication mApplication;
|
||||
|
||||
public ShareModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
private File tempFolder;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "MattermostShare";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
intent.setAction("");
|
||||
intent.removeExtra(Intent.EXTRA_TEXT);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
HashMap<String, Object> constants = new HashMap<>(1);
|
||||
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||
mApplication.sharedExtensionIsOpened = false;
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void close(ReadableMap data) {
|
||||
this.clear();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
currentActivity.finish();
|
||||
}
|
||||
|
||||
if (data != null && data.hasKey("url")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("url");
|
||||
String token = data.getString("token");
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files.size() > 0) {
|
||||
uploadFiles(serverUrl, token, files, postData);
|
||||
} else {
|
||||
try {
|
||||
post(serverUrl, token, postData);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RealPathUtil.deleteTempFiles(this.tempFolder);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void data(Promise promise) {
|
||||
promise.resolve(processIntent());
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getFilePath(String filePath, Promise promise) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
if (currentActivity != null) {
|
||||
Uri uri = Uri.parse(filePath);
|
||||
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
if (path != null) {
|
||||
String text = "file://" + path;
|
||||
map.putString("filePath", text);
|
||||
}
|
||||
}
|
||||
|
||||
promise.resolve(map);
|
||||
}
|
||||
|
||||
public WritableArray processIntent() {
|
||||
WritableMap map = Arguments.createMap();
|
||||
WritableArray items = Arguments.createArray();
|
||||
|
||||
String text = "";
|
||||
String type = "";
|
||||
String action = "";
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
if (type == null) {
|
||||
type = "";
|
||||
}
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
|
||||
text = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
map.putString("value", text);
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map.putString("value", text);
|
||||
|
||||
if (type.equals("image/*")) {
|
||||
type = "image/jpeg";
|
||||
} else if (type.equals("video/*")) {
|
||||
type = "video/mp4";
|
||||
}
|
||||
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
}
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : uris) {
|
||||
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map = Arguments.createMap();
|
||||
text = "file://" + filePath;
|
||||
map.putString("value", text);
|
||||
|
||||
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
|
||||
if (type != null) {
|
||||
if (type.equals("image/*")) {
|
||||
type = "image/jpeg";
|
||||
} else if (type.equals("video/*")) {
|
||||
type = "video/mp4";
|
||||
}
|
||||
} else {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private JSONObject buildPostObject(ReadableMap data) {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
json.put("user_id", data.getString("currentUserId"));
|
||||
json.put("channel_id", data.getString("channelId"));
|
||||
json.put("message", data.getString("value"));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
|
||||
RequestBody body = RequestBody.create(JSON, postData.toString());
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/posts")
|
||||
.post(body)
|
||||
.build();
|
||||
Response response = client.newCall(request).execute();
|
||||
}
|
||||
|
||||
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
|
||||
try {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String filePath = file.getString("fullPath").replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
if (fileInfo.exists()) {
|
||||
final MediaType MEDIA_TYPE = MediaType.parse(file.getString("mimeType"));
|
||||
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(MEDIA_TYPE, fileInfo));
|
||||
}
|
||||
}
|
||||
|
||||
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
|
||||
RequestBody body = builder.build();
|
||||
Request request = new Request.Builder()
|
||||
.header("Authorization", "BEARER " + token)
|
||||
.url(serverUrl + "/api/v4/files")
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = response.body().string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
postData.put("file_ids", file_ids);
|
||||
post(serverUrl, token, postData);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.mattermost.share;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.JavaScriptModule;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.mattermost.rnbeta.MainApplication;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SharePackage implements ReactPackage {
|
||||
MainApplication mApplication;
|
||||
|
||||
public SharePackage(MainApplication application) {
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new ShareModule(mApplication, reactContext));
|
||||
}
|
||||
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 630 B |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 348 B After Width: | Height: | Size: 508 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 33 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 459 KiB |
|
Before Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 971 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 590 KiB |
|
Before Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 468 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 608 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |