Compare commits
8 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78aba1fcb6 | ||
|
|
cba4908854 | ||
|
|
9169f6f694 | ||
|
|
bdc487ab20 | ||
|
|
d7cc95c7f8 | ||
|
|
52ad889bfc | ||
|
|
7be6911b2c | ||
|
|
509dfe5542 |
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,264 @@
|
||||
{
|
||||
"extends": [
|
||||
"./node_modules/eslint-config-mattermost/.eslintrc.json",
|
||||
"./node_modules/eslint-config-mattermost/.eslintrc-react.json"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"pragma": "React",
|
||||
"version": "16.5"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"impliedStrict": true,
|
||||
"modules": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jquery": true,
|
||||
"es6": true,
|
||||
"jest": true
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"after": true,
|
||||
"afterAll": true,
|
||||
"afterEach": true,
|
||||
"before": true,
|
||||
"beforeAll": true,
|
||||
"beforeEach": true,
|
||||
"describe": true,
|
||||
"expect": true,
|
||||
"it": true,
|
||||
"jest": true,
|
||||
"test": 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, "always-multiline"],
|
||||
"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", { "allowArrowFunctions": true }],
|
||||
"generator-star-spacing": [0, {"before": false, "after": true}],
|
||||
"global-require": 0,
|
||||
"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}]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[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.78.0
|
||||
^0.56.0
|
||||
|
||||
12
.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
|
||||
@@ -34,6 +32,7 @@ xcshareddata/
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
.gradle
|
||||
local.properties
|
||||
@@ -69,11 +68,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/report.xml
|
||||
fastlane/Preview.html
|
||||
*/fastlane/screenshots
|
||||
fastlane/.env
|
||||
fastlane/report.xml
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
@@ -86,8 +84,6 @@ ios/sentry.properties
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
#editor-settings
|
||||
.vscode
|
||||
|
||||
|
||||
308
CHANGELOG.md
@@ -1,313 +1,5 @@
|
||||
# Mattermost Mobile Apps Changelog
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
Thank you for your interest in contributing! 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 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.
|
||||
|
||||
### Review Process for this Repo
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
Copyright 2016 Mattermost, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
||||
143
Makefile
@@ -1,18 +1,16 @@
|
||||
.PHONY: pre-run pre-build clean
|
||||
.PHONY: pre-run 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: build-ios build-android unsigned-ios unsigned-android
|
||||
.PHONY: test help
|
||||
|
||||
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
|
||||
|
||||
node_modules: package.json
|
||||
.npminstall: package.json
|
||||
@if ! [ $(shell which npm 2> /dev/null) ]; then \
|
||||
echo "npm is not installed https://npmjs.com"; \
|
||||
exit 1; \
|
||||
@@ -21,14 +19,7 @@ node_modules: package.json
|
||||
@echo Getting Javascript dependencies
|
||||
@npm install
|
||||
|
||||
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)
|
||||
@@ -52,11 +43,9 @@ 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: | .npminstall .podinstall dist/assets ## Installs dependencies and assets
|
||||
|
||||
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
|
||||
|
||||
check-style: node_modules ## Runs eslint
|
||||
check-style: .npminstall ## Runs eslint
|
||||
@echo Checking for style guide compliance
|
||||
@npm run check
|
||||
|
||||
@@ -64,6 +53,7 @@ clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleaning started
|
||||
|
||||
@rm -rf node_modules
|
||||
@rm -f .npminstall
|
||||
@rm -f .podinstall
|
||||
@rm -rf dist
|
||||
@rm -rf ios/build
|
||||
@@ -73,6 +63,11 @@ clean: ## Cleans dependencies, previous builds and temp files
|
||||
@echo Cleanup finished
|
||||
|
||||
post-install:
|
||||
@./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 ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
|
||||
@# 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
|
||||
|
||||
@@ -89,12 +84,24 @@ post-install:
|
||||
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/mattermost-redux && npm run build
|
||||
|
||||
start: | pre-run ## Starts the React Native packager server
|
||||
$(call start_packager)
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
fi
|
||||
|
||||
stop: ## Stops the React Native packager server
|
||||
$(call 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
|
||||
|
||||
check-device-ios:
|
||||
@if ! [ $(shell which xcodebuild) ]; then \
|
||||
@@ -132,7 +139,7 @@ prepare-android-build:
|
||||
run: run-ios ## alias for run-ios
|
||||
|
||||
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 \
|
||||
@if [ $(shell ps -e | 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 \
|
||||
@@ -151,7 +158,7 @@ run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
|
||||
fi
|
||||
|
||||
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 \
|
||||
@if [ $(shell ps -e | 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 \
|
||||
@@ -169,93 +176,51 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
|
||||
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)
|
||||
build-ios: | pre-run check-style ## Creates an iOS build
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@echo "Building iOS app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
|
||||
$(call stop_packager)
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
|
||||
$(call start_packager)
|
||||
build-android: | pre-run check-style prepare-android-build ## Creates an Android build
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@echo "Building Android app"
|
||||
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
|
||||
$(call stop_packager)
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
|
||||
$(call start_packager)
|
||||
unsigned-ios: pre-run check-style
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@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 .
|
||||
@rm -rf build-ios/
|
||||
$(call stop_packager)
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
|
||||
$(call start_packager)
|
||||
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
|
||||
@mkdir -p build-ios
|
||||
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
|
||||
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
|
||||
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
|
||||
@rm -rf build-ios/
|
||||
$(call stop_packager)
|
||||
|
||||
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
|
||||
$(call start_packager)
|
||||
unsigned-android: pre-run check-style prepare-android-build
|
||||
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
fi
|
||||
@echo "Building unsigned Android app"
|
||||
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
|
||||
@mv android/app/build/outputs/apk/unsigned/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
|
||||
$(call stop_packager)
|
||||
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
|
||||
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
|
||||
|
||||
test: | pre-run check-style ## Runs tests
|
||||
@npm test
|
||||
|
||||
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
|
||||
$(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)
|
||||
|
||||
can-build-pr:
|
||||
@if [ -z ${PR_ID} ]; then \
|
||||
echo a PR number needs to be specified; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
@[[ -d $(MM_UTILITIES_DIR) ]] || echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
|
||||
@[[ -d $(MM_UTILITIES_DIR) ]] && cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-mobile
|
||||
|
||||
|
||||
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
define start_packager
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
|
||||
echo Starting React Native packager server; \
|
||||
npm start & echo; \
|
||||
else \
|
||||
echo React Native packager server already running; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define stop_packager
|
||||
@echo Stopping React Native packager server
|
||||
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
|
||||
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
|
||||
echo React Native packager server stopped; \
|
||||
else \
|
||||
echo No React Native packager server running; \
|
||||
fi
|
||||
endef
|
||||
|
||||
3687
NOTICE.txt
17
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+
|
||||
|
||||
**Supported iOS versions:** 9.3+
|
||||
**Supported Android versions:** 5.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).
|
||||
|
||||
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/).
|
||||
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://about.mattermost.com/default-enterprise-app-store).
|
||||
|
||||
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
|
||||
@@ -21,7 +20,7 @@ To help with testing app updates before they're released, you can:
|
||||
1. Sign up to be a beta tester
|
||||
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
|
||||
- [iOS](https://mattermost-fastlane.herokuapp.com/)
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
|
||||
2. Install the `Mattermost Beta` app. New updates in the Beta app are released each Monday. You will receive a notification when the new updates are available.
|
||||
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
|
||||
- Device information
|
||||
- Repro steps
|
||||
@@ -32,9 +31,9 @@ To help with testing app updates before they're released, you can:
|
||||
|
||||
### Contribute Code
|
||||
|
||||
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues) 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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -74,8 +74,8 @@ import com.android.build.OutputFile
|
||||
|
||||
project.ext.react = [
|
||||
entryFile: "index.js",
|
||||
bundleCommand: "ram-bundle",
|
||||
bundleConfig: "packager-config.js"
|
||||
bundleCommand: "unbundle",
|
||||
bundleConfig: "packager/config.js"
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
@@ -106,21 +106,15 @@ def enableSeparateBuildPerCPUArchitecture = false
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'lib/x86_64/libjsc.so'
|
||||
pickFirst 'lib/arm64-v8a/libjsc.so'
|
||||
}
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 182
|
||||
versionName "1.17.0"
|
||||
multiDexEnabled = true
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 23
|
||||
versionCode 122
|
||||
versionName "1.9.3"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
@@ -157,7 +151,6 @@ android {
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
signingConfig null
|
||||
matchingFallbacks = ['debug', 'release']
|
||||
}
|
||||
}
|
||||
// applicationVariants are e.g. debug, release
|
||||
@@ -185,60 +178,46 @@ configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'android-jsc') {
|
||||
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
|
||||
}
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
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'
|
||||
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r216113'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
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-recyclerview-list')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-gesture-handler')
|
||||
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-document-picker')
|
||||
compile project(':react-native-keychain')
|
||||
compile project(':react-native-doc-viewer')
|
||||
compile project(':react-native-video')
|
||||
compile project(':react-native-navigation')
|
||||
compile project(':react-native-image-picker')
|
||||
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
|
||||
@@ -247,5 +226,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
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,32 +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="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
>
|
||||
<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" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.react_native_interface;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
/**
|
||||
* ResolvePromise: Helper class that abstracts boilerplate
|
||||
@@ -17,41 +16,16 @@ public class ResolvePromise implements Promise {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, Throwable e, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message, WritableMap map) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String message) {
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.NotificationChannel;
|
||||
import android.content.Intent;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -47,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +118,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
channelIdToNotificationCount.remove(channelId);
|
||||
channelIdToNotification.remove(channelId);
|
||||
digestNotification();
|
||||
clearAllNotifications();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -155,19 +140,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
String packageName = mContext.getPackageName();
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
|
||||
|
||||
String CHANNEL_ID = "channel_01";
|
||||
String CHANNEL_NAME = "Mattermost notifications";
|
||||
|
||||
// First, get a builder initialized with defaults from the core class.
|
||||
final Notification.Builder notification = new Notification.Builder(mContext);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
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");
|
||||
|
||||
@@ -193,11 +167,9 @@ public class CustomPushNotification extends PushNotification {
|
||||
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;
|
||||
@@ -223,8 +195,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
if (numberString != null) {
|
||||
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
int numMessages = getMessageCountInChannel(channelId);
|
||||
@@ -300,13 +271,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
PendingIntent replyPendingIntent;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
replyPendingIntent = PendingIntent.getForegroundService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
} else {
|
||||
replyPendingIntent = PendingIntent.getService(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")
|
||||
@@ -371,11 +336,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);
|
||||
String numberString = data.getString("badge");
|
||||
if (numberString != null) {
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
|
||||
}
|
||||
|
||||
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
|
||||
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,15 +1,10 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.mattermost.share.SharePackage;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
|
||||
import com.oblador.keychain.KeychainPackage;
|
||||
@@ -22,9 +17,6 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
|
||||
import com.RNFetchBlob.RNFetchBlobPackage;
|
||||
import com.gantix.JailMonkey.JailMonkeyPackage;
|
||||
import io.tradle.react.LocalAuthPackage;
|
||||
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
@@ -39,15 +31,14 @@ 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 android.util.Log;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
@@ -75,7 +66,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new JailMonkeyPackage(),
|
||||
new RNFetchBlobPackage(),
|
||||
new MattermostPackage(this),
|
||||
new RNSentryPackage(),
|
||||
new RNSentryPackage(this),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube(),
|
||||
new ReactVideoPackage(),
|
||||
@@ -83,10 +74,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new ReactNativeDocumentPicker(),
|
||||
new SharePackage(this),
|
||||
new KeychainPackage(),
|
||||
new InitializationPackage(this),
|
||||
new RNRecyclerviewListPackage(),
|
||||
new RNCWebViewPackage(),
|
||||
new RNGestureHandlerPackage()
|
||||
new InitializationPackage(this)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,12 +87,6 @@ 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
|
||||
@@ -114,7 +96,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearHostOnActivityDestroy(Activity activity) {
|
||||
public boolean clearHostOnActivityDestroy() {
|
||||
// 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;
|
||||
@@ -130,9 +112,4 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new JsIOHelper()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
|
||||
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +1,9 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.RemoteInput;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
@@ -22,28 +18,6 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
public class NotificationReplyService extends HeadlessJsTaskService {
|
||||
private Context mContext;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
Context mContext = this.getApplicationContext();
|
||||
final Resources res = mContext.getResources();
|
||||
String packageName = mContext.getPackageName();
|
||||
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
|
||||
String CHANNEL_ID = "Reply job";
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||
Notification notification =
|
||||
new Notification.Builder(mContext, CHANNEL_ID)
|
||||
.setContentTitle("Replying to message")
|
||||
.setContentText(packageName)
|
||||
.setSmallIcon(smallIconResId)
|
||||
.build();
|
||||
startForeground(1, notification);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
||||
mContext = getApplicationContext();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -70,13 +69,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
|
||||
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
|
||||
LayoutParams.FLAG_SECURE);
|
||||
}
|
||||
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
|
||||
if (managedConfig!= null && managedConfig.size() > 0 && activity != null) {
|
||||
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
|
||||
}
|
||||
|
||||
if (activity != null) {
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -45,7 +44,9 @@ public class RealPathUtil {
|
||||
return id.replaceFirst("raw:", "");
|
||||
}
|
||||
try {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
|
||||
return null;
|
||||
@@ -94,33 +95,15 @@ public class RealPathUtil {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
String fileName = uri.getLastPathSegment();
|
||||
File cacheDir = new File(context.getCacheDir(), "mmShare");
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(uri.getPath());
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
|
||||
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
@@ -205,12 +188,8 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
ContentResolver cR = context.getContentResolver();
|
||||
return cR.getType(uri);
|
||||
}
|
||||
|
||||
public static void deleteTempFiles(final File dir) {
|
||||
|
||||
@@ -77,7 +77,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
this.clear();
|
||||
getCurrentActivity().finish();
|
||||
|
||||
if (data != null && data.hasKey("url")) {
|
||||
if (data != null) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("url");
|
||||
String token = data.getString("token");
|
||||
@@ -145,19 +145,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
@@ -167,15 +165,12 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
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";
|
||||
if (type.equals("image/*")) {
|
||||
type = "image/jpeg";
|
||||
} else if (type.equals("video/*")) {
|
||||
type = "video/mp4";
|
||||
}
|
||||
|
||||
map.putString("type", type);
|
||||
items.pushMap(map);
|
||||
}
|
||||
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 459 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 585 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 590 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 468 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Executable file → Normal file
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 608 KiB |
@@ -1,3 +1,4 @@
|
||||
<?xml version="1.0"?>
|
||||
<resources>
|
||||
<string name="app_name">Mattermost Beta</string>
|
||||
<string name="inAppPinCode_title">in-App Pincode</string>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
<!-- Trust preinstalled CAs -->
|
||||
<certificates src="system" />
|
||||
<!-- Additionally trust user added CAs -->
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -1,42 +1,20 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "28.0.3"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 26
|
||||
supportLibVersion = "28.0.0"
|
||||
}
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.1'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath 'com.android.tools.build:gradle:2.2.+'
|
||||
classpath 'com.google.gms:google-services:3.1.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
maven {
|
||||
@@ -45,7 +23,7 @@ allprojects {
|
||||
}
|
||||
maven {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url "$rootDir/../node_modules/jsc-android/dist"
|
||||
url "$rootDir/../node_modules/jsc-android/android"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
#android.enableAapt2=false
|
||||
#android.useDeprecatedNdk=true
|
||||
android.useDeprecatedNdk=true
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
|
||||
110
android/gradlew
vendored
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
@@ -6,6 +6,47 @@
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
@@ -20,49 +61,9 @@ while [ -h "$PRG" ] ; do
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
cd "$SAVED" >&-
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
@@ -89,7 +90,7 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
@@ -113,7 +114,6 @@ fi
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
@@ -154,19 +154,11 @@ if $cygwin ; then
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
|
||||
14
android/gradlew.bat
vendored
@@ -8,14 +8,14 @@
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
@@ -46,9 +46,10 @@ echo location of your Java installation.
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
@@ -59,6 +60,11 @@ set _SKIP=2
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
rootProject.name = 'Mattermost'
|
||||
include ':react-native-gesture-handler'
|
||||
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
|
||||
include ':react-native-document-picker'
|
||||
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
|
||||
include ':react-native-keychain'
|
||||
@@ -15,8 +13,8 @@ include ':react-native-sentry'
|
||||
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
|
||||
include ':react-native-exception-handler'
|
||||
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
|
||||
include ':rn-fetch-blob'
|
||||
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
|
||||
include ':react-native-fetch-blob'
|
||||
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
|
||||
include ':jail-monkey'
|
||||
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
|
||||
include ':react-native-local-auth'
|
||||
@@ -41,7 +39,3 @@ include ':react-native-svg'
|
||||
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
|
||||
include ':react-native-linear-gradient'
|
||||
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||
include ':react-native-recyclerview-list'
|
||||
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {networkStatusChangedAction} from 'redux-offline';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
export function connection(isOnline) {
|
||||
return async (dispatch) => {
|
||||
Client4.setOnline(isOnline);
|
||||
dispatch(networkStatusChangedAction(isOnline));
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: DeviceTypes.CONNECTION_CHANGED,
|
||||
data: isOnline,
|
||||
});
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
35
app/actions/views/account_notifications.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {updateMe} from 'mattermost-redux/actions/users';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
|
||||
export function handleUpdateUserNotifyProps(notifyProps) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const config = state.entities.general.config;
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
const {interval, user_id: userId, ...otherProps} = notifyProps;
|
||||
|
||||
const email = notifyProps.email;
|
||||
if (config.EnableEmailBatching === 'true' && email !== 'false') {
|
||||
const emailInterval = [{
|
||||
user_id: userId,
|
||||
category: Preferences.CATEGORY_NOTIFICATIONS,
|
||||
name: Preferences.EMAIL_INTERVAL,
|
||||
value: interval,
|
||||
}];
|
||||
|
||||
savePreferences(currentUserId, emailInterval)(dispatch, getState);
|
||||
}
|
||||
|
||||
const props = {...otherProps, email};
|
||||
try {
|
||||
await updateMe({notify_props: props})(dispatch, getState);
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
@@ -9,8 +9,8 @@ import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
markChannelAsRead,
|
||||
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
|
||||
selectChannel,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
@@ -18,8 +18,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getChannel, getCurrentChannelId, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
getChannelByName,
|
||||
@@ -160,7 +160,6 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
const state = getState();
|
||||
const {posts, postsInChannel} = state.entities.posts;
|
||||
const postsIds = postsInChannel[channelId];
|
||||
const actions = [];
|
||||
|
||||
const time = Date.now();
|
||||
|
||||
@@ -171,15 +170,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
|
||||
if (received) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
type: ViewTypes.SET_INITIAL_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count,
|
||||
},
|
||||
});
|
||||
loadMorePostsVisible = received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
}
|
||||
} else {
|
||||
const {lastConnectAt} = state.device.websocket;
|
||||
@@ -199,34 +190,25 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
|
||||
|
||||
if (received) {
|
||||
const count = received.order.length;
|
||||
loadMorePostsVisible = postsIds.length + count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
actions.push({
|
||||
type: ViewTypes.SET_INITIAL_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: postsIds.length + count,
|
||||
},
|
||||
});
|
||||
loadMorePostsVisible = postsIds.length + received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
if (received) {
|
||||
actions.push({
|
||||
dispatch({
|
||||
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
||||
channelId,
|
||||
time,
|
||||
});
|
||||
}
|
||||
|
||||
actions.push(setLoadMorePostsVisible(loadMorePostsVisible));
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(setLoadMorePostsVisible(loadMorePostsVisible));
|
||||
};
|
||||
}
|
||||
|
||||
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
const {data} = await dispatch(action); // eslint-disable-line no-await-in-loop
|
||||
const {data} = await action(dispatch, getState);
|
||||
|
||||
if (data) {
|
||||
dispatch(setChannelRetryFailed(false));
|
||||
@@ -243,8 +225,8 @@ export function loadFilesForPostIfNecessary(postId) {
|
||||
const {files} = getState().entities;
|
||||
const fileIdsForPost = files.fileIdsByPostId[postId];
|
||||
|
||||
if (!fileIdsForPost?.length) {
|
||||
await dispatch(getFilesForPost(postId));
|
||||
if (!fileIdsForPost) {
|
||||
await getFilesForPost(postId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -277,43 +259,10 @@ export function selectInitialChannel(teamId) {
|
||||
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, lastChannel);
|
||||
|
||||
if (
|
||||
myMembers[lastChannelId] &&
|
||||
lastChannel &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
|
||||
) {
|
||||
dispatch(handleSelectChannel(lastChannelId));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(selectDefaultChannel(teamId));
|
||||
};
|
||||
}
|
||||
|
||||
export function selectPenultimateChannel(teamId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
|
||||
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length > 1 ? lastChannelForTeam[1] : '';
|
||||
const lastChannel = channels[lastChannelId];
|
||||
|
||||
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
|
||||
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
|
||||
|
||||
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
|
||||
isGroupChannelVisible(myPreferences, lastChannel);
|
||||
|
||||
if (
|
||||
myMembers[lastChannelId] &&
|
||||
lastChannel &&
|
||||
lastChannel.delete_at === 0 &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
|
||||
) {
|
||||
dispatch(setChannelLoading(true));
|
||||
dispatch(handleSelectChannel(lastChannelId));
|
||||
if (lastChannelId && myMembers[lastChannelId] &&
|
||||
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
|
||||
handleSelectChannel(lastChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId)(dispatch, getState);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,7 +272,7 @@ export function selectPenultimateChannel(teamId) {
|
||||
|
||||
export function selectDefaultChannel(teamId) {
|
||||
return (dispatch, getState) => {
|
||||
const {channels} = getState().entities.channels;
|
||||
const channels = getState().entities.channels.channels;
|
||||
|
||||
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
|
||||
let channelId;
|
||||
@@ -339,30 +288,23 @@ export function selectDefaultChannel(teamId) {
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
dispatch(setChannelDisplayName(''));
|
||||
dispatch(handleSelectChannel(channelId));
|
||||
dispatch(markChannelAsRead(channelId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
export function handleSelectChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const channel = getChannel(state, channelId);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const sameChannel = channelId === currentChannelId;
|
||||
const member = getMyChannelMember(state, channelId);
|
||||
const {currentTeamId} = getState().entities.teams;
|
||||
|
||||
dispatch(setLoadMorePostsVisible(true));
|
||||
|
||||
// If the app is open from push notification, we already fetched the posts.
|
||||
if (!fromPushNotification) {
|
||||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
}
|
||||
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
|
||||
selectChannel(channelId)(dispatch, getState);
|
||||
|
||||
dispatch(batchActions([
|
||||
selectChannel(channelId),
|
||||
setChannelDisplayName(channel.display_name),
|
||||
{
|
||||
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
|
||||
data: channelId,
|
||||
@@ -373,19 +315,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
teamId: currentTeamId,
|
||||
channelId,
|
||||
},
|
||||
{
|
||||
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
|
||||
data: channelId,
|
||||
member,
|
||||
},
|
||||
]));
|
||||
|
||||
let markPreviousChannelId;
|
||||
if (!fromPushNotification && !sameChannel) {
|
||||
markPreviousChannelId = currentChannelId;
|
||||
}
|
||||
|
||||
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -410,13 +340,6 @@ export function insertToDraft(value) {
|
||||
};
|
||||
}
|
||||
|
||||
export function markChannelViewedAndRead(channelId, previousChannelId, markOnServer = true) {
|
||||
return (dispatch) => {
|
||||
dispatch(markChannelAsRead(channelId, previousChannelId, markOnServer));
|
||||
dispatch(markChannelAsViewed(channelId, previousChannelId));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleDMChannel(otherUserId, visible, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -434,7 +357,7 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
|
||||
value: Date.now().toString(),
|
||||
}];
|
||||
|
||||
dispatch(savePreferences(currentUserId, dm));
|
||||
savePreferences(currentUserId, dm)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -450,7 +373,7 @@ export function toggleGMChannel(channelId, visible) {
|
||||
value: visible,
|
||||
}];
|
||||
|
||||
dispatch(savePreferences(currentUserId, gm));
|
||||
savePreferences(currentUserId, gm)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -459,9 +382,9 @@ export function closeDMChannel(channel) {
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
dispatch(toggleDMChannel(channel.teammate_id, 'false'));
|
||||
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
|
||||
if (channel.id === currentChannelId) {
|
||||
dispatch(selectInitialChannel(state.entities.teams.currentTeamId));
|
||||
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -505,7 +428,7 @@ export function leaveChannel(channel, reset = false) {
|
||||
await dispatch(selectDefaultChannel(currentTeamId));
|
||||
}
|
||||
|
||||
await dispatch(serviceLeaveChannel(channel.id));
|
||||
await serviceLeaveChannel(channel.id)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -545,12 +468,13 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
const currentPostVisibility = postVisibility[channelId] || 0;
|
||||
|
||||
if (loadingPosts[channelId]) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we already have the posts that we want to show
|
||||
if (!focusedPostId) {
|
||||
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
|
||||
const postsInChannel = state.entities.posts.postsInChannel[channelId] || [];
|
||||
const loadedPostCount = postsInChannel.length;
|
||||
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
|
||||
if (loadedPostCount >= desiredPostVisibility) {
|
||||
@@ -575,9 +499,9 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
|
||||
let result;
|
||||
if (focusedPostId) {
|
||||
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
|
||||
result = await getPostsBefore(channelId, focusedPostId, page, pageSize)(dispatch, getState);
|
||||
} else {
|
||||
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
|
||||
result = await getPosts(channelId, page, pageSize)(dispatch, getState);
|
||||
}
|
||||
|
||||
const actions = [{
|
||||
@@ -586,28 +510,18 @@ export function increasePostVisibility(channelId, focusedPostId) {
|
||||
channelId,
|
||||
}];
|
||||
|
||||
let hasMorePost = false;
|
||||
if (result) {
|
||||
const count = result.order.length;
|
||||
hasMorePost = count >= pageSize;
|
||||
|
||||
actions.push({
|
||||
type: ViewTypes.INCREASE_POST_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count,
|
||||
},
|
||||
});
|
||||
|
||||
const posts = result.data;
|
||||
if (posts) {
|
||||
// make sure to increment the posts visibility
|
||||
// only if we got results
|
||||
actions.push(doIncreasePostVisibility(channelId));
|
||||
|
||||
actions.push(setLoadMorePostsVisible(hasMorePost));
|
||||
actions.push(setLoadMorePostsVisible(posts.order.length >= pageSize));
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
return hasMorePost;
|
||||
|
||||
return Boolean(posts);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {addChannelMember} from 'mattermost-redux/actions/channels';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {removeChannelMember} from 'mattermost-redux/actions/channels';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
@@ -28,12 +27,6 @@ export function executeCommand(message, channelId, rootId) {
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data.trigger_id) {
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
return {data, error};
|
||||
return await executeCommandService(msg, args)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {handleSelectChannel, setChannelDisplayName} from './channel';
|
||||
import {createChannel} from 'mattermost-redux/actions/channels';
|
||||
@@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
|
||||
type,
|
||||
};
|
||||
|
||||
const {data} = await dispatch(createChannel(channel, currentUserId));
|
||||
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
|
||||
if (data && data.id) {
|
||||
dispatch(setChannelDisplayName(displayName));
|
||||
dispatch(handleSelectChannel(data.id));
|
||||
handleSelectChannel(data.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {updateMe, setDefaultProfileImage} from 'mattermost-redux/actions/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
|
||||
import {buildFileUploadData} from 'app/utils/file';
|
||||
|
||||
export function updateUser(user, success, error) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -18,22 +17,9 @@ export function updateUser(user, success, error) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setProfileImageUri(imageUri = '') {
|
||||
return {
|
||||
type: ViewTypes.SET_PROFILE_IMAGE_URI,
|
||||
imageUri,
|
||||
export function handleUploadProfileImage(image, userId) {
|
||||
return async (dispatch, getState) => {
|
||||
const imageData = buildFileUploadData(image);
|
||||
return await uploadProfileImage(userId, imageData)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function removeProfileImage(user) {
|
||||
return async (dispatch) => {
|
||||
const result = await dispatch(setDefaultProfileImage(user));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
updateUser,
|
||||
setProfileImageUri,
|
||||
removeProfileImage,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FileTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {getSessions} from 'mattermost-redux/actions/users';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {app} from 'app/mattermost';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -35,8 +30,7 @@ export function handlePasswordChanged(password) {
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
const {config, license} = state.entities.general;
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
const deviceToken = state.entities.general.deviceToken;
|
||||
@@ -44,22 +38,17 @@ export function handleSuccessfulLogin() {
|
||||
|
||||
app.setAppCredentials(deviceToken, currentUserId, token, url);
|
||||
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
if (enableTimezone) {
|
||||
dispatch(autoUpdateTimezone(getDeviceTimezone()));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
|
||||
data: {
|
||||
url,
|
||||
token,
|
||||
},
|
||||
});
|
||||
}, getState);
|
||||
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
getDataRetentionPolicy()(dispatch, getState);
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
@@ -68,46 +57,22 @@ export function handleSuccessfulLogin() {
|
||||
};
|
||||
}
|
||||
|
||||
export function scheduleExpiredNotification(intl) {
|
||||
return (dispatch, getState) => {
|
||||
export function getSession() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {deviceToken} = state.entities.general;
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
|
||||
});
|
||||
const {credentials} = state.entities.general;
|
||||
const token = credentials && credentials.token;
|
||||
|
||||
// Once the user logs in we are going to wait for 10 seconds
|
||||
// before retrieving the session that belongs to this device
|
||||
// to ensure that we get the actual session without issues
|
||||
// then we can schedule the local notification for the session expired
|
||||
setTimeout(async () => {
|
||||
let sessions;
|
||||
try {
|
||||
sessions = await dispatch(getSessions(currentUserId));
|
||||
} catch (e) {
|
||||
console.warn('Failed to get current session', e); // eslint-disable-line no-console
|
||||
return;
|
||||
if (currentUserId && token) {
|
||||
const session = await Client4.getSessions(currentUserId, token);
|
||||
if (Array.isArray(session) && session[0]) {
|
||||
const s = session[0];
|
||||
return s.expires_at;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(sessions.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = sessions.data.find((s) => s.device_id === deviceToken);
|
||||
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
|
||||
|
||||
if (expiresAt) {
|
||||
PushNotifications.localNotificationSchedule({
|
||||
date: new Date(expiresAt),
|
||||
message,
|
||||
userInfo: {
|
||||
localNotification: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,5 +80,5 @@ export default {
|
||||
handleLoginIdChanged,
|
||||
handlePasswordChanged,
|
||||
handleSuccessfulLogin,
|
||||
scheduleExpiredNotification,
|
||||
getSession,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See License.txt for license information.
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
handlePasswordChanged,
|
||||
} from 'app/actions/views/login';
|
||||
|
||||
jest.mock('react-native-fetch-blob/fs', () => ({
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: () => jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/mattermost', () => ({
|
||||
app: {
|
||||
setAppCredentials: () => jest.fn(),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
|
||||
|
||||
export function makeDirectChannel(otherUserId, switchToChannel = true) {
|
||||
export function makeDirectChannel(otherUserId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
@@ -23,11 +23,11 @@ export function makeDirectChannel(otherUserId, switchToChannel = true) {
|
||||
|
||||
dispatch(toggleDMChannel(otherUserId, 'true', channel.id));
|
||||
} else {
|
||||
result = await dispatch(createDirectChannel(currentUserId, otherUserId));
|
||||
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
|
||||
channel = result.data;
|
||||
}
|
||||
|
||||
if (channel && switchToChannel) {
|
||||
if (channel) {
|
||||
dispatch(handleSelectChannel(channel.id));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {PostTypes} from 'mattermost-redux/action_types';
|
||||
import {doPostAction} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {generateId} from 'app/utils/file';
|
||||
|
||||
@@ -40,31 +37,3 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options) {
|
||||
return {
|
||||
type: ViewTypes.SELECTED_ACTION_MENU,
|
||||
data: {
|
||||
dataSource,
|
||||
onSelect,
|
||||
options,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function selectAttachmentMenuAction(postId, actionId, text, value) {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
|
||||
postId,
|
||||
data: {
|
||||
[actionId]: {
|
||||
text,
|
||||
value,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(doPostAction(postId, actionId, value));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
|
||||
import {fetchMyChannelsAndMembers, markChannelAsRead} from 'mattermost-redux/actions/channels';
|
||||
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {getPosts} from 'mattermost-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {recordTime} from 'app/utils/segment';
|
||||
|
||||
import {handleSelectChannel} from 'app/actions/views/channel';
|
||||
import {
|
||||
handleSelectChannel,
|
||||
setChannelDisplayName,
|
||||
retryGetPostsAction,
|
||||
} from 'app/actions/views/channel';
|
||||
|
||||
export function startDataCleanup() {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -46,21 +51,16 @@ export function loadConfigAndLicense() {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFromPushNotification(notification, startAppFromPushNotification) {
|
||||
export function loadFromPushNotification(notification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {data} = notification;
|
||||
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
|
||||
const {channels} = state.entities.channels;
|
||||
const {currentChannelId, channels} = state.entities.channels;
|
||||
const channelId = data.channel_id;
|
||||
|
||||
let channelId = '';
|
||||
let teamId = currentTeamId;
|
||||
if (data) {
|
||||
channelId = data.channel_id;
|
||||
|
||||
// when the notification does not have a team id is because its from a DM or GM
|
||||
teamId = data.team_id || currentTeamId;
|
||||
}
|
||||
// when the notification does not have a team id is because its from a DM or GM
|
||||
const teamId = data.team_id || currentTeamId;
|
||||
|
||||
// load any missing data
|
||||
const loading = [];
|
||||
@@ -83,7 +83,17 @@ export function loadFromPushNotification(notification, startAppFromPushNotificat
|
||||
dispatch(selectTeam({id: teamId}));
|
||||
}
|
||||
|
||||
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
|
||||
// when the notification is from the same channel as the current channel
|
||||
// we should get the posts
|
||||
if (channelId === currentChannelId) {
|
||||
dispatch(markChannelAsRead(channelId, null, false));
|
||||
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
} else {
|
||||
// when the notification is from a channel other than the current channel
|
||||
dispatch(markChannelAsRead(channelId, currentChannelId, false));
|
||||
dispatch(setChannelDisplayName(''));
|
||||
dispatch(handleSelectChannel(channelId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,10 +101,8 @@ export function purgeOfflineStore() {
|
||||
return {type: General.OFFLINE_STORE_PURGE};
|
||||
}
|
||||
|
||||
// A non-optimistic version of the createPost action in mattermost-redux with the file handling
|
||||
// removed since it's not needed.
|
||||
export function createPostForNotificationReply(post) {
|
||||
return async (dispatch, getState) => {
|
||||
export function createPost(post) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = state.entities.users.currentUserId;
|
||||
|
||||
@@ -108,23 +116,18 @@ export function createPostForNotificationReply(post) {
|
||||
update_at: timestamp,
|
||||
};
|
||||
|
||||
try {
|
||||
const data = await Client4.createPost({...newPost, create_at: 0});
|
||||
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
|
||||
dispatch({
|
||||
type: PostTypes.RECEIVED_POSTS,
|
||||
data: {
|
||||
order: [],
|
||||
posts: {
|
||||
[data.id]: data,
|
||||
[payload.id]: payload,
|
||||
},
|
||||
},
|
||||
channelId: data.channel_id,
|
||||
channelId: payload.channel_id,
|
||||
});
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,13 +139,6 @@ export function recordLoadTime(screenName, category) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setDeepLinkURL(url) {
|
||||
return {
|
||||
type: ViewTypes.SET_DEEP_LINK_URL,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
loadConfigAndLicense,
|
||||
loadFromPushNotification,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -13,25 +11,3 @@ export function handleSearchDraftChanged(text) {
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function showSearchModal(navigator, initialValue = '') {
|
||||
return (dispatch, getState) => {
|
||||
const theme = getTheme(getState());
|
||||
|
||||
const options = {
|
||||
screen: 'Search',
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
overrideBackPress: true,
|
||||
passProps: {
|
||||
initialValue,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
};
|
||||
|
||||
navigator.showModal(options);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import configureStore from 'redux-mock-store';
|
||||
@@ -11,6 +11,13 @@ import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {handleServerUrlChanged} from 'app/actions/views/select_server';
|
||||
|
||||
jest.mock('react-native-fetch-blob/fs', () => ({
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: () => jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('Actions.Views.SelectServer', () => {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {TeamTypes} from 'mattermost-redux/action_types';
|
||||
import {getMyTeams} from 'mattermost-redux/actions/teams';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {selectFirstAvailableTeam} from 'app/utils/teams';
|
||||
|
||||
export function handleTeamChange(teamId) {
|
||||
import {setChannelDisplayName} from './channel';
|
||||
|
||||
export function handleTeamChange(teamId, selectChannel = true) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
@@ -18,7 +20,19 @@ export function handleTeamChange(teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({type: TeamTypes.SELECT_TEAM, data: teamId});
|
||||
const actions = [setChannelDisplayName(''), {type: TeamTypes.SELECT_TEAM, data: teamId}];
|
||||
|
||||
if (selectChannel) {
|
||||
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
|
||||
|
||||
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
|
||||
const lastChannelId = lastChannels[0] || '';
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
markChannelAsViewed(currentChannelId)(dispatch, getState);
|
||||
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,34 +40,23 @@ export function selectDefaultTeam() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const {ExperimentalPrimaryTeam} = getConfig(state);
|
||||
const {ExperimentalPrimaryTeam} = state.entities.general.config;
|
||||
const {teams: allTeams, myMembers} = state.entities.teams;
|
||||
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
|
||||
|
||||
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
|
||||
let defaultTeam;
|
||||
if (ExperimentalPrimaryTeam) {
|
||||
defaultTeam = teams.find((t) => t.name === ExperimentalPrimaryTeam.toLowerCase());
|
||||
}
|
||||
|
||||
if (!defaultTeam) {
|
||||
defaultTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
|
||||
}
|
||||
|
||||
if (defaultTeam) {
|
||||
dispatch(handleTeamChange(defaultTeam.id));
|
||||
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
|
||||
handleTeamChange(defaultTeam.id)(dispatch, getState);
|
||||
} else {
|
||||
// If for some reason we reached this point cause of a failure in rehydration or something
|
||||
// lets fetch the teams one more time to make sure the user does not belong to any team
|
||||
const {data, error} = await dispatch(getMyTeams());
|
||||
if (error) {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
|
||||
}
|
||||
|
||||
if (defaultTeam) {
|
||||
dispatch(handleTeamChange(defaultTeam.id));
|
||||
} else {
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
|
||||
}
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See License.txt for license information.
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
handleCommentDraftSelectionChanged,
|
||||
} from 'app/actions/views/thread';
|
||||
|
||||
jest.mock('react-native-fetch-blob/fs', () => ({
|
||||
dirs: {
|
||||
DocumentDir: () => jest.fn(),
|
||||
CacheDir: () => jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('Actions.Views.Thread', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
export function setCurrentUserStatusOffline() {
|
||||
return (dispatch, getState) => {
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
|
||||
return dispatch({
|
||||
type: UserTypes.RECEIVED_STATUS,
|
||||
data: {
|
||||
user_id: currentUserId,
|
||||
status: General.OFFLINE,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
|
||||
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
jest.mock('mattermost-redux/actions/users', () => ({
|
||||
getStatus: (...args) => ({type: 'MOCK_GET_STATUS', args}),
|
||||
getStatusesByIds: (...args) => ({type: 'MOCK_GET_STATUS_BY_IDS', args}),
|
||||
startPeriodicStatusUpdates: () => ({type: 'MOCK_PERIODIC_STATUS_UPDATES'}),
|
||||
}));
|
||||
|
||||
describe('Actions.Views.User', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current-user-id',
|
||||
statuses: {
|
||||
'current-user-id': 'online',
|
||||
'another-user-id1': 'away',
|
||||
'another-user-id2': 'dnd',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should set the current user as offline', async () => {
|
||||
const action = {
|
||||
type: UserTypes.RECEIVED_STATUS,
|
||||
data: {
|
||||
user_id: 'current-user-id',
|
||||
status: General.OFFLINE,
|
||||
},
|
||||
};
|
||||
|
||||
await store.dispatch(setCurrentUserStatusOffline());
|
||||
expect(store.getActions()).toEqual([action]);
|
||||
});
|
||||
});
|
||||
90
app/app.js
@@ -1,15 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// See License.txt for license information.
|
||||
|
||||
/* eslint-disable global-require*/
|
||||
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
|
||||
import {AsyncStorage, NativeModules} from 'react-native';
|
||||
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
|
||||
|
||||
import {loadMe} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {setDeepLinkURL} from 'app/actions/views/root';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import tracker from 'app/utils/time_tracker';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
@@ -51,48 +50,10 @@ export default class App {
|
||||
this.token = null;
|
||||
this.url = null;
|
||||
|
||||
// Load polyfill for iOS 9
|
||||
if (Platform.OS === 'ios') {
|
||||
const majorVersionIOS = parseInt(Platform.Version, 10);
|
||||
if (majorVersionIOS < 10) {
|
||||
require('@babel/polyfill');
|
||||
}
|
||||
}
|
||||
|
||||
// Usage deeplinking
|
||||
Linking.addEventListener('url', this.handleDeepLink);
|
||||
|
||||
this.setFontFamily();
|
||||
this.getStartupThemes();
|
||||
this.getAppCredentials();
|
||||
}
|
||||
|
||||
setFontFamily = () => {
|
||||
// Set a global font for Android
|
||||
if (Platform.OS === 'android') {
|
||||
const defaultFontFamily = {
|
||||
style: {
|
||||
fontFamily: 'Roboto',
|
||||
},
|
||||
};
|
||||
const TextRender = Text.render;
|
||||
const initialDefaultProps = Text.defaultProps;
|
||||
Text.defaultProps = {
|
||||
...initialDefaultProps,
|
||||
...defaultFontFamily,
|
||||
};
|
||||
Text.render = function render(props, ...args) {
|
||||
const oldProps = props;
|
||||
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
|
||||
try {
|
||||
return Reflect.apply(TextRender, this, [newProps, ...args]);
|
||||
} finally {
|
||||
newProps = oldProps;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getTranslations = () => {
|
||||
if (this.translations) {
|
||||
return this.translations;
|
||||
@@ -130,20 +91,13 @@ export default class App {
|
||||
const [deviceToken, currentUserId] = usernameParsed;
|
||||
const [token, url] = passwordParsed;
|
||||
|
||||
// if for any case the url and the token aren't valid proceed with re-hydration
|
||||
if (url && url !== 'undefined' && token && token !== 'undefined') {
|
||||
this.deviceToken = deviceToken;
|
||||
this.currentUserId = currentUserId;
|
||||
this.token = token;
|
||||
this.url = url;
|
||||
Client4.setUrl(url);
|
||||
Client4.setToken(token);
|
||||
} else {
|
||||
this.waitForRehydration = true;
|
||||
}
|
||||
this.deviceToken = deviceToken;
|
||||
this.currentUserId = currentUserId;
|
||||
this.token = token;
|
||||
this.url = url;
|
||||
Client4.setUrl(url);
|
||||
Client4.setToken(token);
|
||||
}
|
||||
} else {
|
||||
this.waitForRehydration = false;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
@@ -198,7 +152,6 @@ export default class App {
|
||||
if (!currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = `${deviceToken}, ${currentUserId}`;
|
||||
const password = `${token},${url}`;
|
||||
|
||||
@@ -208,14 +161,7 @@ export default class App {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
// Only save to keychain if the url and token are set
|
||||
if (url && token) {
|
||||
try {
|
||||
setGenericPassword(username, password);
|
||||
} catch (e) {
|
||||
console.warn('could not set credentials', e); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
setGenericPassword(username, password);
|
||||
};
|
||||
|
||||
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
|
||||
@@ -269,11 +215,6 @@ export default class App {
|
||||
]);
|
||||
};
|
||||
|
||||
handleDeepLink = (event) => {
|
||||
const {url} = event;
|
||||
store.dispatch(setDeepLinkURL(url));
|
||||
}
|
||||
|
||||
launchApp = async () => {
|
||||
const shouldStart = await handleManagedConfig();
|
||||
if (shouldStart) {
|
||||
@@ -288,21 +229,11 @@ export default class App {
|
||||
|
||||
const {dispatch} = store;
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
dispatch(setDeepLinkURL(url));
|
||||
});
|
||||
|
||||
let screen = 'SelectServer';
|
||||
if (this.token && this.url) {
|
||||
screen = 'Channel';
|
||||
tracker.initialLoad = Date.now();
|
||||
|
||||
try {
|
||||
dispatch(loadMe());
|
||||
} catch (e) {
|
||||
// Fall through since we should have a previous version of the current user because we have a token
|
||||
console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console
|
||||
}
|
||||
dispatch(loadMe());
|
||||
}
|
||||
|
||||
switch (screen) {
|
||||
@@ -314,6 +245,7 @@ export default class App {
|
||||
break;
|
||||
}
|
||||
|
||||
this.setStartAppFromPushNotification(false);
|
||||
this.setAppStarted(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Badge should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#444",
|
||||
"borderRadius": 20,
|
||||
"height": 20,
|
||||
"padding": 12,
|
||||
"paddingBottom": 3,
|
||||
"paddingTop": 3,
|
||||
"position": "absolute",
|
||||
"right": 30,
|
||||
"top": 2,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
},
|
||||
Object {
|
||||
"opacity": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "center",
|
||||
"flex": 1,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"textAlignVertical": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
onLayout={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "white",
|
||||
"fontSize": 14,
|
||||
},
|
||||
Object {
|
||||
"color": "#145dbf",
|
||||
"fontSize": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -1,57 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`profile_picture_button should match snapshot 1`] = `
|
||||
<AttachmentButton
|
||||
blurTextBox={[MockFunction]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={
|
||||
Array [
|
||||
null,
|
||||
]
|
||||
}
|
||||
maxFileCount={5}
|
||||
maxFileSize={20971520}
|
||||
navigator={
|
||||
Object {
|
||||
"dismissModal": [MockFunction],
|
||||
"push": [MockFunction],
|
||||
"setButtons": [MockFunction],
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[MockFunction]}
|
||||
/>
|
||||
`;
|
||||
@@ -1,62 +1,365 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 1`] = `
|
||||
<AnimatedComponent
|
||||
style={
|
||||
Array [
|
||||
ShallowWrapper {
|
||||
"length": 1,
|
||||
Symbol(enzyme.__root__): [Circular],
|
||||
Symbol(enzyme.__unrendered__): <AnnouncementBanner
|
||||
actions={
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"dismissBanner": [MockFunction],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
allowDismissal={true}
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={true}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
/>,
|
||||
Symbol(enzyme.__renderer__): Object {
|
||||
"batchedUpdates": [Function],
|
||||
"getNode": [Function],
|
||||
"render": [Function],
|
||||
"simulateEvent": [Function],
|
||||
"unmount": [Function],
|
||||
},
|
||||
Symbol(enzyme.__node__): Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"children": <TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Banner Text
|
||||
</Text>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>,
|
||||
"style": Array [
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"activeOpacity": 0.2,
|
||||
"children": Array [
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Banner Text
|
||||
</Text>,
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>,
|
||||
],
|
||||
"onPress": [Function],
|
||||
"style": Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": Array [
|
||||
Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"accessible": true,
|
||||
"allowFontScaling": true,
|
||||
"children": "Banner Text",
|
||||
"ellipsizeMode": "tail",
|
||||
"numberOfLines": 1,
|
||||
"style": Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": "Banner Text",
|
||||
"type": [Function],
|
||||
},
|
||||
Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"allowFontScaling": false,
|
||||
"color": "#fff",
|
||||
"name": "info",
|
||||
"size": 16,
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": null,
|
||||
"type": [Function],
|
||||
},
|
||||
],
|
||||
"type": [Function],
|
||||
},
|
||||
"type": [Function],
|
||||
},
|
||||
Symbol(enzyme.__nodes__): Array [
|
||||
Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"children": <TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Banner Text
|
||||
</Text>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>,
|
||||
"style": Array [
|
||||
Object {
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 10,
|
||||
"position": "absolute",
|
||||
"top": 0,
|
||||
"width": "100%",
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
"backgroundColor": "#ddd",
|
||||
"height": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<RemoveMarkdown
|
||||
value="Banner Text"
|
||||
/>
|
||||
</Text>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</AnimatedComponent>
|
||||
],
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"activeOpacity": 0.2,
|
||||
"children": Array [
|
||||
<Text
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Banner Text
|
||||
</Text>,
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#fff"
|
||||
name="info"
|
||||
size={16}
|
||||
/>,
|
||||
],
|
||||
"onPress": [Function],
|
||||
"style": Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
},
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": Array [
|
||||
Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"accessible": true,
|
||||
"allowFontScaling": true,
|
||||
"children": "Banner Text",
|
||||
"ellipsizeMode": "tail",
|
||||
"numberOfLines": 1,
|
||||
"style": Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"fontSize": 14,
|
||||
"marginRight": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "#fff",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": "Banner Text",
|
||||
"type": [Function],
|
||||
},
|
||||
Object {
|
||||
"instance": null,
|
||||
"key": undefined,
|
||||
"nodeType": "class",
|
||||
"props": Object {
|
||||
"allowFontScaling": false,
|
||||
"color": "#fff",
|
||||
"name": "info",
|
||||
"size": 16,
|
||||
},
|
||||
"ref": null,
|
||||
"rendered": null,
|
||||
"type": [Function],
|
||||
},
|
||||
],
|
||||
"type": [Function],
|
||||
},
|
||||
"type": [Function],
|
||||
},
|
||||
],
|
||||
Symbol(enzyme.__options__): Object {
|
||||
"adapter": ReactSixteenAdapter {
|
||||
"options": Object {
|
||||
"enableComponentDidUpdateOnSetState": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`AnnouncementBanner should match snapshot 2`] = `null`;
|
||||
exports[`AnnouncementBanner should match snapshot 2`] = `
|
||||
ShallowWrapper {
|
||||
"length": 1,
|
||||
Symbol(enzyme.__root__): [Circular],
|
||||
Symbol(enzyme.__unrendered__): <AnnouncementBanner
|
||||
actions={
|
||||
Object {
|
||||
"dismissBanner": [MockFunction],
|
||||
}
|
||||
}
|
||||
allowDismissal={true}
|
||||
bannerColor="#ddd"
|
||||
bannerDismissed={false}
|
||||
bannerEnabled={false}
|
||||
bannerText="Banner Text"
|
||||
bannerTextColor="#fff"
|
||||
/>,
|
||||
Symbol(enzyme.__renderer__): Object {
|
||||
"batchedUpdates": [Function],
|
||||
"getNode": [Function],
|
||||
"render": [Function],
|
||||
"simulateEvent": [Function],
|
||||
"unmount": [Function],
|
||||
},
|
||||
Symbol(enzyme.__node__): null,
|
||||
Symbol(enzyme.__nodes__): Array [
|
||||
null,
|
||||
],
|
||||
Symbol(enzyme.__options__): Object {
|
||||
"adapter": ReactSixteenAdapter {
|
||||
"options": Object {
|
||||
"enableComponentDidUpdateOnSetState": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Alert,
|
||||
Animated,
|
||||
StyleSheet,
|
||||
Text,
|
||||
@@ -12,19 +13,19 @@ import {
|
||||
import {intlShape} from 'react-intl';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
|
||||
import RemoveMarkdown from 'app/components/remove_markdown';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class AnnouncementBanner extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
dismissBanner: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
allowDismissal: PropTypes.bool,
|
||||
bannerColor: PropTypes.string,
|
||||
bannerDismissed: PropTypes.bool,
|
||||
bannerEnabled: PropTypes.bool,
|
||||
bannerText: PropTypes.string,
|
||||
bannerTextColor: PropTypes.string,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -51,24 +52,30 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handlePress = () => {
|
||||
const {navigator, theme} = this.props;
|
||||
handleDismiss = () => {
|
||||
const {actions, bannerText} = this.props;
|
||||
actions.dismissBanner(bannerText);
|
||||
};
|
||||
|
||||
navigator.push({
|
||||
screen: 'ExpandedAnnouncementBanner',
|
||||
title: this.context.intl.formatMessage({
|
||||
id: 'mobile.announcement_banner.title',
|
||||
defaultMessage: 'Announcement',
|
||||
}),
|
||||
animated: true,
|
||||
backButtonTitle: '',
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
});
|
||||
handlePress = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = [{
|
||||
text: formatMessage({id: 'mobile.announcement_banner.ok', defaultMessage: 'OK'}),
|
||||
}];
|
||||
|
||||
if (this.props.allowDismissal) {
|
||||
options.push({
|
||||
text: formatMessage({id: 'mobile.announcement_banner.dismiss', defaultMessage: 'Dismiss'}),
|
||||
onPress: this.handleDismiss,
|
||||
});
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
formatMessage({id: 'mobile.announcement_banner.title', defaultMessage: 'Announcement'}),
|
||||
this.props.bannerText,
|
||||
options,
|
||||
{cancelable: false}
|
||||
);
|
||||
};
|
||||
|
||||
toggleBanner = (show = true) => {
|
||||
@@ -113,7 +120,7 @@ export default class AnnouncementBanner extends PureComponent {
|
||||
numberOfLines={1}
|
||||
style={[style.bannerText, bannerTextStyle]}
|
||||
>
|
||||
<RemoveMarkdown value={bannerText}/>
|
||||
{bannerText}
|
||||
</Text>
|
||||
<MaterialIcons
|
||||
color={bannerTextColor}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import {configure, shallow} from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
configure({adapter: new Adapter()});
|
||||
|
||||
import AnnouncementBanner from './announcement_banner.js';
|
||||
|
||||
@@ -12,13 +12,15 @@ jest.useFakeTimers();
|
||||
|
||||
describe('AnnouncementBanner', () => {
|
||||
const baseProps = {
|
||||
actions: {
|
||||
dismissBanner: jest.fn(),
|
||||
},
|
||||
allowDismissal: true,
|
||||
bannerColor: '#ddd',
|
||||
bannerDismissed: false,
|
||||
bannerEnabled: true,
|
||||
bannerText: 'Banner Text',
|
||||
bannerTextColor: '#fff',
|
||||
navigator: {},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -26,9 +28,21 @@ describe('AnnouncementBanner', () => {
|
||||
<AnnouncementBanner {...baseProps}/>
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
wrapper.setProps({bannerEnabled: false});
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('should call actions.dismissBanner on handleDismiss', () => {
|
||||
const actions = {dismissBanner: jest.fn()};
|
||||
const props = {...baseProps, actions};
|
||||
const wrapper = shallow(
|
||||
<AnnouncementBanner {...props}/>
|
||||
);
|
||||
|
||||
wrapper.instance().handleDismiss();
|
||||
expect(actions.dismissBanner).toHaveBeenCalledTimes(1);
|
||||
expect(actions.dismissBanner).toHaveBeenCalledWith(props.bannerText);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {dismissBanner} from 'app/actions/views/announcement';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner';
|
||||
|
||||
@@ -14,13 +16,21 @@ function mapStateToProps(state) {
|
||||
const {announcement} = state.views;
|
||||
|
||||
return {
|
||||
allowDismissal: config.AllowBannerDismissal === 'true',
|
||||
bannerColor: config.BannerColor,
|
||||
bannerDismissed: config.BannerText === announcement,
|
||||
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
|
||||
bannerText: config.BannerText,
|
||||
bannerTextColor: config.BannerTextColor || '#000',
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(AnnouncementBanner);
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
dismissBanner,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -10,7 +10,6 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -18,6 +17,7 @@ export default class AtMention extends React.PureComponent {
|
||||
mentionName: PropTypes.string.isRequired,
|
||||
mentionStyle: CustomPropTypes.Style,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onLongPress: PropTypes.func.isRequired,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: CustomPropTypes.Style,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
@@ -74,7 +74,7 @@ export default class AtMention extends React.PureComponent {
|
||||
};
|
||||
|
||||
getUserDetailsFromMentionName(props) {
|
||||
let mentionName = props.mentionName.toLowerCase();
|
||||
let mentionName = props.mentionName;
|
||||
|
||||
while (mentionName.length > 0) {
|
||||
if (props.usersByUsername.hasOwnProperty(mentionName)) {
|
||||
@@ -95,33 +95,26 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleLongPress = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {intl} = this.context;
|
||||
|
||||
const config = await mattermostManaged.getLocalConfig();
|
||||
|
||||
let action;
|
||||
if (config.copyAndPasteProtection !== 'false') {
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const actionText = formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'});
|
||||
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value !== 1) {
|
||||
this.handleCopyMention();
|
||||
}
|
||||
});
|
||||
action = {
|
||||
text: intl.formatMessage({
|
||||
id: 'mobile.mention.copy_mention',
|
||||
defaultMessage: 'Copy Mention',
|
||||
}),
|
||||
onPress: this.handleCopyMention,
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onLongPress(action);
|
||||
};
|
||||
|
||||
handleCopyMention = () => {
|
||||
const {user} = this.state;
|
||||
const {mentionName} = this.props;
|
||||
let username = mentionName;
|
||||
if (user.username) {
|
||||
username = user.username;
|
||||
}
|
||||
|
||||
const {username} = this.state;
|
||||
Clipboard.setString(`@${username}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
@@ -10,8 +8,6 @@ import {
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import {DocumentPicker} from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
@@ -19,58 +15,35 @@ import Permissions from 'react-native-permissions';
|
||||
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
canBrowseFiles: PropTypes.bool,
|
||||
canBrowsePhotoLibrary: PropTypes.bool,
|
||||
canBrowseVideoLibrary: PropTypes.bool,
|
||||
canTakePhoto: PropTypes.bool,
|
||||
canTakeVideo: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
wrapper: PropTypes.bool,
|
||||
extraOptions: PropTypes.arrayOf(PropTypes.object),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
|
||||
canBrowseFiles: true,
|
||||
canBrowsePhotoLibrary: true,
|
||||
canBrowseVideoLibrary: true,
|
||||
canTakePhoto: true,
|
||||
canTakeVideo: true,
|
||||
maxFileCount: 5,
|
||||
extraOptions: null,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
attachPhotoFromCamera = () => {
|
||||
return this.attachFileFromCamera('photo', 'camera');
|
||||
};
|
||||
|
||||
attachFileFromCamera = async (mediaType, source) => {
|
||||
attachFileFromCamera = async () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
videoQuality: 'high',
|
||||
quality: 1.0,
|
||||
noData: true,
|
||||
mediaType,
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true,
|
||||
@@ -92,7 +65,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
},
|
||||
};
|
||||
|
||||
const hasPhotoPermission = await this.hasPhotoPermission(source);
|
||||
const hasPhotoPermission = await this.hasPhotoPermission();
|
||||
|
||||
if (hasPhotoPermission) {
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
@@ -108,7 +81,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
attachFileFromLibrary = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
quality: 0.8,
|
||||
quality: 1.0,
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
title: formatMessage({
|
||||
@@ -140,14 +113,10 @@ export default class AttachmentButton extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromCamera = () => {
|
||||
return this.attachFileFromCamera('video', 'camera');
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const options = {
|
||||
videoQuality: 'high',
|
||||
quality: 1.0,
|
||||
mediaType: 'video',
|
||||
noData: true,
|
||||
permissionDenied: {
|
||||
@@ -177,12 +146,11 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
|
||||
attachFileFromFiles = async () => {
|
||||
const {browseFileTypes} = this.props;
|
||||
const hasPermission = await this.hasStoragePermission();
|
||||
|
||||
if (hasPermission) {
|
||||
DocumentPicker.show({
|
||||
filetype: [browseFileTypes],
|
||||
filetype: [Platform.OS === 'ios' ? 'public.item' : '*/*'],
|
||||
}, async (error, res) => {
|
||||
if (error) {
|
||||
return;
|
||||
@@ -206,11 +174,11 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
hasPhotoPermission = async (source) => {
|
||||
hasPhotoPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check(source || 'photo');
|
||||
const hasPermissionToStorage = await Permissions.check('photo');
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
@@ -295,13 +263,13 @@ export default class AttachmentButton extends PureComponent {
|
||||
defaultMessage: 'To upload images from your Android device, please change your permission settings.',
|
||||
}),
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.android.permission_denied_dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
},
|
||||
grantOption,
|
||||
]
|
||||
);
|
||||
return false;
|
||||
@@ -312,20 +280,8 @@ export default class AttachmentButton extends PureComponent {
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
uploadFiles = (images) => {
|
||||
this.props.uploadFiles(images);
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
@@ -344,17 +300,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
|
||||
showFileAttachmentOptions = () => {
|
||||
const {
|
||||
canBrowseFiles,
|
||||
canBrowsePhotoLibrary,
|
||||
canBrowseVideoLibrary,
|
||||
canTakePhoto,
|
||||
canTakeVideo,
|
||||
fileCount,
|
||||
maxFileCount,
|
||||
onShowFileMaxWarning,
|
||||
extraOptions,
|
||||
} = this.props;
|
||||
const {fileCount, maxFileCount, onShowFileMaxWarning} = this.props;
|
||||
|
||||
if (fileCount === maxFileCount) {
|
||||
onShowFileMaxWarning();
|
||||
@@ -362,77 +308,50 @@ export default class AttachmentButton extends PureComponent {
|
||||
}
|
||||
|
||||
this.props.blurTextBox();
|
||||
const items = [];
|
||||
|
||||
if (canTakePhoto) {
|
||||
items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
|
||||
const options = {
|
||||
items: [{
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera_photo'),
|
||||
defaultMessage: 'Take Photo',
|
||||
id: 'mobile.file_upload.camera',
|
||||
defaultMessage: 'Take Photo or Video',
|
||||
},
|
||||
icon: 'camera',
|
||||
});
|
||||
}
|
||||
|
||||
if (canTakeVideo) {
|
||||
items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
|
||||
text: {
|
||||
id: t('mobile.file_upload.camera_video'),
|
||||
defaultMessage: 'Take Video',
|
||||
},
|
||||
icon: 'video-camera',
|
||||
});
|
||||
}
|
||||
|
||||
if (canBrowsePhotoLibrary) {
|
||||
items.push({
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
id: t('mobile.file_upload.library'),
|
||||
id: 'mobile.file_upload.library',
|
||||
defaultMessage: 'Photo Library',
|
||||
},
|
||||
icon: 'photo',
|
||||
});
|
||||
}
|
||||
}],
|
||||
};
|
||||
|
||||
if (canBrowseVideoLibrary && Platform.OS === 'android') {
|
||||
items.push({
|
||||
if (Platform.OS === 'android') {
|
||||
options.items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
|
||||
text: {
|
||||
id: t('mobile.file_upload.video'),
|
||||
id: 'mobile.file_upload.video',
|
||||
defaultMessage: 'Video Library',
|
||||
},
|
||||
icon: 'file-video-o',
|
||||
});
|
||||
}
|
||||
|
||||
if (canBrowseFiles) {
|
||||
items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
|
||||
text: {
|
||||
id: t('mobile.file_upload.browse'),
|
||||
defaultMessage: 'Browse Files',
|
||||
},
|
||||
icon: 'file',
|
||||
});
|
||||
}
|
||||
|
||||
if (extraOptions) {
|
||||
extraOptions.forEach((option) => {
|
||||
if (option !== null) {
|
||||
items.push(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
options.items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
|
||||
text: {
|
||||
id: 'mobile.file_upload.browse',
|
||||
defaultMessage: 'Browse Files',
|
||||
},
|
||||
icon: 'file',
|
||||
});
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
items,
|
||||
items: options.items,
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
@@ -490,3 +409,4 @@ const style = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -13,7 +13,6 @@ import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divide
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
export default class AtMention extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -26,8 +25,8 @@ export default class AtMention extends PureComponent {
|
||||
defaultChannel: PropTypes.object,
|
||||
inChannel: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
listHeight: PropTypes.number,
|
||||
matchTerm: PropTypes.string,
|
||||
maxListHeight: PropTypes.number,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
outChannel: PropTypes.array,
|
||||
@@ -82,7 +81,7 @@ export default class AtMention extends PureComponent {
|
||||
const sections = [];
|
||||
if (isSearch) {
|
||||
sections.push({
|
||||
id: t('mobile.suggestion.members'),
|
||||
id: 'mobile.suggestion.members',
|
||||
defaultMessage: 'Members',
|
||||
data: teamMembers,
|
||||
key: 'teamMembers',
|
||||
@@ -90,7 +89,7 @@ export default class AtMention extends PureComponent {
|
||||
} else {
|
||||
if (inChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.members'),
|
||||
id: 'suggestion.mention.members',
|
||||
defaultMessage: 'Channel Members',
|
||||
data: inChannel,
|
||||
key: 'inChannel',
|
||||
@@ -99,7 +98,7 @@ export default class AtMention extends PureComponent {
|
||||
|
||||
if (this.checkSpecialMentions(matchTerm)) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.special'),
|
||||
id: 'suggestion.mention.special',
|
||||
defaultMessage: 'Special Mentions',
|
||||
data: this.getSpecialMentions(),
|
||||
key: 'special',
|
||||
@@ -109,7 +108,7 @@ export default class AtMention extends PureComponent {
|
||||
|
||||
if (outChannel.length) {
|
||||
sections.push({
|
||||
id: t('suggestion.mention.nonmembers'),
|
||||
id: 'suggestion.mention.nonmembers',
|
||||
defaultMessage: 'Not in Channel',
|
||||
data: outChannel,
|
||||
key: 'outChannel',
|
||||
@@ -132,18 +131,18 @@ export default class AtMention extends PureComponent {
|
||||
getSpecialMentions = () => {
|
||||
return [{
|
||||
completeHandle: 'all',
|
||||
id: t('suggestion.mention.all'),
|
||||
id: 'suggestion.mention.all',
|
||||
defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team',
|
||||
values: {
|
||||
townsquare: this.props.defaultChannel.display_name,
|
||||
},
|
||||
}, {
|
||||
completeHandle: 'channel',
|
||||
id: t('suggestion.mention.channel'),
|
||||
id: 'suggestion.mention.channel',
|
||||
defaultMessage: 'Notifies everyone in the channel',
|
||||
}, {
|
||||
completeHandle: 'here',
|
||||
id: t('suggestion.mention.here'),
|
||||
id: 'suggestion.mention.here',
|
||||
defaultMessage: 'Notifies everyone in the channel and online',
|
||||
}];
|
||||
};
|
||||
@@ -204,7 +203,7 @@ export default class AtMention extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {maxListHeight, theme} = this.props;
|
||||
const {isSearch, listHeight, theme} = this.props;
|
||||
const {mentionComplete, sections} = this.state;
|
||||
|
||||
if (sections.length === 0 || mentionComplete) {
|
||||
@@ -219,7 +218,7 @@ export default class AtMention extends PureComponent {
|
||||
<SectionList
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
|
||||
sections={sections}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
@@ -235,5 +234,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
search: {
|
||||
minHeight: 125,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
|
||||