Compare commits

..

9 Commits

Author SHA1 Message Date
Elias Nahum
6e08d69b10 Bump app version to 1.6.1 2018-02-12 16:15:53 -03:00
Elias Nahum
a8a95a98e7 Bump app build to 86 2018-02-12 15:21:22 -03:00
Stephen Kiers
b4b23d4d46 ICU-639- Fixes bug with Samsung keyboard (#1427)
* Internalized value to local state so that there is less ‘delay’ and less surface area for bugs to happen

* removed toLowercase on comparison and fixed parent components instead

* Update more_channels.js
2018-02-12 14:04:17 -03:00
enahum
99cff03193 Fix Android stuck when opening from push notification (#1429) 2018-02-12 14:04:02 -03:00
enahum
b9864bf26f Fix Android app stuck in splash screen when killed by the OS (#1403) 2018-02-12 09:09:07 -03:00
enahum
e73b9017fc Fix race condition preventing the app opened from a PN to go to the correct channel (#1398)
* Fix race condition preventing the app opened from a PN to go to the correct channel

* Feedback review
2018-02-12 09:08:23 -03:00
enahum
f65820dd0c Bump Android build number to 85 (#1417) 2018-02-05 21:54:11 -03:00
enahum
7564ac023c Bump iOS build number to 85 (#1416) 2018-02-05 21:46:49 -03:00
enahum
5ef83639e9 Remove iOS share extension (#1415) 2018-02-05 21:14:09 -03:00
977 changed files with 66137 additions and 81774 deletions

16
.babelrc Normal file
View File

@@ -0,0 +1,16 @@
{
"presets": [ "react-native" ],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
},
"plugins": [
["module-resolver", {
"root": ["./src", "."],
"alias": {
"assets": "./dist/assets"
}
}]
]
}

View File

@@ -11,7 +11,7 @@ charset = utf-8
indent_style = space
indent_size = 4
[{package.json,.eslintrc.json}]
[webapp/package.json]
indent_size = 2
[Makefile]

View File

@@ -1,31 +1,262 @@
{
"extends": [
"./node_modules/eslint-config-mattermost/.eslintrc.json",
"./node_modules/eslint-config-mattermost/.eslintrc-react.json"
],
"settings": {
"react": {
"pragma": "React",
"version": "16.5"
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"impliedStrict": true,
"modules": true
}
},
"parser": "babel-eslint",
"plugins": [
"react",
"mocha"
],
"env": {
"browser": true,
"node": true,
"jquery": true,
"es6": true
},
"globals": {
"jest": true,
"describe": true,
"it": true,
"expect": true,
"before": true,
"beforeEach": true,
"after": true,
"afterEach": true
},
"rules": {
"array-bracket-spacing": [2, "never"],
"array-callback-return": 2,
"arrow-body-style": 0,
"arrow-parens": [2, "always"],
"arrow-spacing": [2, { "before": true, "after": true }],
"block-scoped-var": 2,
"brace-style": [2, "1tbs", { "allowSingleLine": false }],
"camelcase": [2, {"properties": "never"}],
"class-methods-use-this": 0,
"comma-dangle": [2, "never"],
"comma-spacing": [2, {"before": false, "after": true}],
"comma-style": [2, "last"],
"complexity": [1, 10],
"computed-property-spacing": [2, "never"],
"consistent-return": 2,
"consistent-this": [2, "self"],
"constructor-super": 2,
"curly": [2, "all"],
"dot-location": [2, "object"],
"dot-notation": 2,
"eqeqeq": [2, "smart"],
"func-call-spacing": [2, "never"],
"func-names": 2,
"func-style": [2, "declaration"],
"generator-star-spacing": [0, {"before": false, "after": true}],
"global-require": 2,
"guard-for-in": 2,
"id-blacklist": 0,
"indent": [2, 4, {"SwitchCase": 0}],
"jsx-quotes": [2, "prefer-single"],
"key-spacing": [2, {"beforeColon": false, "afterColon": true, "mode": "strict"}],
"keyword-spacing": [2, {"before": true, "after": true, "overrides": {}}],
"line-comment-position": 0,
"linebreak-style": 2,
"lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": true, "allowBlockStart": true, "allowBlockEnd": true }],
"max-lines": [1, {"max": 450, "skipBlankLines": true, "skipComments": false}],
"max-nested-callbacks": [2, {"max":2}],
"max-statements-per-line": [2, {"max": 1}],
"multiline-ternary": [1, "never"],
"new-cap": 2,
"new-parens": 2,
"newline-before-return": 0,
"newline-per-chained-call": 0,
"no-alert": 2,
"no-array-constructor": 2,
"no-caller": 2,
"no-case-declarations": 2,
"no-class-assign": 2,
"no-cond-assign": [2, "except-parens"],
"no-confusing-arrow": 2,
"no-console": 2,
"no-const-assign": 2,
"no-constant-condition": 2,
"no-debugger": 2,
"no-div-regex": 2,
"no-dupe-args": 2,
"no-dupe-class-members": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-duplicate-imports": [2, {"includeExports": true}],
"no-else-return": 2,
"no-empty": 2,
"no-empty-function": 2,
"no-empty-pattern": 2,
"no-eval": 2,
"no-ex-assign": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-extra-label": 2,
"no-extra-parens": 0,
"no-extra-semi": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-func-assign": 2,
"no-global-assign": 2,
"no-implicit-coercion": 2,
"no-implicit-globals": 0,
"no-implied-eval": 2,
"no-inner-declarations": 0,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-lonely-if": 2,
"no-loop-func": 2,
"no-magic-numbers": 0,
"no-mixed-operators": [2, {"allowSamePrecedence": false}],
"no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": [2, { "exceptions": { "Property": false } }],
"no-multi-str": 0,
"no-multiple-empty-lines": [2, {"max": 1}],
"no-native-reassign": 2,
"no-negated-condition": 2,
"no-nested-ternary": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-object": 2,
"no-new-symbol": 2,
"no-new-wrappers": 2,
"no-octal-escape": 2,
"no-param-reassign": 2,
"no-process-env": 2,
"no-process-exit": 2,
"no-proto": 2,
"no-redeclare": 2,
"no-return-assign": [2, "always"],
"no-script-url": 2,
"no-self-assign": [2, {"props": true}],
"no-self-compare": 2,
"no-sequences": 2,
"no-shadow": [2, {"hoist": "functions"}],
"no-shadow-restricted-names": 2,
"no-spaced-func": 2,
"no-tabs": 0,
"no-template-curly-in-string": 2,
"no-ternary": 0,
"no-this-before-super": 2,
"no-throw-literal": 0,
"no-trailing-spaces": [2, { "skipBlankLines": false }],
"no-undef-init": 2,
"no-undefined": 2,
"no-underscore-dangle": 2,
"no-unexpected-multiline": 2,
"no-unmodified-loop-condition": 2,
"no-unneeded-ternary": [2, {"defaultAssignment": false}],
"no-unreachable": 2,
"no-unsafe-finally": 2,
"no-unsafe-negation": 2,
"no-unused-expressions": 2,
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}],
"no-use-before-define": [2, {"classes": false, "functions": false, "variables": false}],
"no-useless-computed-key": 2,
"no-useless-concat": 2,
"no-useless-constructor": 2,
"no-useless-escape": 2,
"no-useless-rename": 2,
"no-var": 0,
"no-void": 2,
"no-warning-comments": 1,
"no-whitespace-before-property": 2,
"no-with": 2,
"object-curly-newline": 0,
"object-curly-spacing": [2, "never"],
"object-property-newline": [2, {"allowMultiplePropertiesPerLine": true}],
"object-shorthand": [2, "always"],
"one-var": [2, "never"],
"one-var-declaration-per-line": 0,
"operator-linebreak": [2, "after"],
"padded-blocks": [2, "never"],
"prefer-arrow-callback": 2,
"prefer-const": 2,
"prefer-numeric-literals": 2,
"prefer-reflect": 2,
"prefer-rest-params": 2,
"prefer-spread": 2,
"prefer-template": 0,
"quote-props": [2, "as-needed"],
"quotes": [2, "single", "avoid-escape"],
"radix": 2,
"react/display-name": [2, { "ignoreTranspilerName": false }],
"react/jsx-boolean-value": [2, "always"],
"react/jsx-closing-bracket-location": [2, { "location": "tag-aligned" }],
"react/jsx-curly-spacing": [2, "never"],
"react/jsx-equals-spacing": [2, "never"],
"react/jsx-filename-extension": [2, {"extensions": [".js"]}],
"react/jsx-first-prop-new-line": [2, "multiline"],
"react/jsx-handler-names": 0,
"react/jsx-indent": [2, 4],
"react/jsx-indent-props": [2, 4],
"react/jsx-key": 2,
"react/jsx-max-props-per-line": [2, { "maximum": 1 }],
"react/jsx-no-bind": 0,
"react/jsx-no-duplicate-props": [2, { "ignoreCase": false }],
"react/jsx-no-literals": 2,
"react/jsx-no-target-blank": 2,
"react/jsx-no-undef": 2,
"react/jsx-pascal-case": 2,
"react/jsx-tag-spacing": [2, {"closingSlash": "never", "beforeSelfClosing": "never", "afterOpening": "never"}],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/jsx-no-comment-textnodes": 2,
"react/no-danger": 0,
"react/no-deprecated": 2,
"react/no-did-mount-set-state": 2,
"react/no-did-update-set-state": 2,
"react/no-direct-mutation-state": 2,
"react/no-is-mounted": 2,
"react/no-multi-comp": [2, { "ignoreStateless": true }],
"react/no-render-return-value": 2,
"react/no-set-state": 0,
"react/no-string-refs": 0,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
"react/prefer-stateless-function": 0,
"react/prop-types": 2,
"react/require-optimization": 1,
"react/require-render-return": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 0,
"react/jsx-wrap-multilines": 2,
"react/no-find-dom-node": 1,
"react/forbid-component-props": 0,
"react/no-danger-with-children": 2,
"react/no-unused-prop-types": [1, {"skipShapeProps": true}],
"react/style-prop-object": 2,
"react/no-children-prop": 2,
"react/no-unescaped-entities": 2,
"require-yield": 2,
"rest-spread-spacing": [2, "never"],
"semi": [2, "always"],
"semi-spacing": [2, {"before": false, "after": true}],
"sort-imports": 0,
"sort-keys": 0,
"space-before-blocks": [2, "always"],
"space-before-function-paren": [2, {"anonymous": "never", "named": "never", "asyncArrow": "always"}],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"symbol-description": 2,
"template-curly-spacing": [2, "never"],
"valid-typeof": [2, {"requireStringLiterals": false}],
"vars-on-top": 0,
"wrap-iife": [2, "outside"],
"wrap-regex": 2,
"yoda": [2, "never", {"exceptRange": false, "onlyEquality": false}],
"mocha/no-exclusive-tests": 2
}
},
"env": {
"jest": true
},
"globals": {
"__DEV__": true
},
"rules": {
"global-require": 0,
"react/display-name": [2, { "ignoreTranspilerName": false }],
"react/jsx-filename-extension": [2, {"extensions": [".js"]}]
},
"overrides": [
{
"files": ["*.test.js", "*.test.jsx"],
"env": {
"jest": true
}
}
]
}

View File

@@ -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

17
.gitignore vendored
View File

@@ -1,6 +1,5 @@
assets/override
dist
build-ios
*.zip
server.PID
mattermost.keystore
@@ -21,7 +20,6 @@ build/
*.perspectivev3
!default.perspectivev3
xcuserdata
xcshareddata
*.xccheckout
*.moved-aside
DerivedData
@@ -30,10 +28,10 @@ DerivedData
*.apk
*.xcuserstate
project.xcworkspace
xcshareddata/
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
@@ -69,11 +67,10 @@ tags
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
fastlane/.env
fastlane/report.xml
fastlane/Preview.html
*/fastlane/screenshots
# Sentry
android/sentry.properties
@@ -81,14 +78,10 @@ ios/sentry.properties
# Testing
.nyc_output
coverage
# Pods
.podinstall
ios/Pods/
# Bundle artifact
*.jsbundle
#editor-settings
.vscode
.vscode

View File

@@ -1,496 +1,5 @@
# Mattermost Mobile Apps Changelog
## 1.17.0 Release
- Release Date: March 20, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
- Fixed support for EMM connections using VPN on-demand to indicate that every request should wait for the VPN connection to be establish and to set the value in seconds for the timeout. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Highlights
- iOS Share Extension now supports large file sizes and improved performance
### Bug Fixes
- Fixed support for EMM connections using VPN on-demand. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Fixed several Android app crash / fatal error issues.
- Fixed an issue on Android where the app crashed intermittently when selecting a link.
- Fixed an issue where email notifications setting was out of sync with the webapp until the setting was edited.
- Fixed an issue where notification badges were not cleared from other clients when clicking on a push notification after opening the mobile app.
- Fixed an issue where the app did not show local notification when session expired.
- Fixed an issue where the profile picture for webhooks was showing the hook owner picture.
- Fixed an issue where some emoji were not rendered as jumbo.
- Fixed an issue where jumbo emoji posted as a reply sometimes appeared with large space beneath.
- Fixed an issue where the "No Internet Connection" banner did not always display when internet connectivity was lost.
- Fixed an issue where the "No Internet Connection" banner did not always disappear when connection was re-estabilished.
- Fixed an issue where opening channels with unreads had loading indicator placed above unread messages line.
## 1.16.1 Release
- Release Date: February 21, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue where link previews and reactions weren't displayed when post metadata was disabled.
- Fixed an issue on Android where the app crashed when sharing multiple files.
## 1.16.0 Release
- Release Date: February 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Improvements
- Added the ability to remove own profile picture.
- Changed "X" to "Cancel" on Edit Profile page.
- Added support for relative permalinks.
### Bug Fixes
- Fixed an issue where the iOS app did not wait until the on-demand VPN connection was established. (EMM Providers)
- Fixed an issue with a white screen caused by missing Russian translations.
- Fixed an issue where the iOS badge notification did not always clear.
- Fixed an issue where the thread view displayed a new message indicator.
- Fixed an issue where quick multiple taps on the file icon opened multiple file previews.
- Fixed an issue where the settings page did not show an option to join other teams.
- Fixed an issue where image previews didn't work after using Delete File Cache.
- Fixed an issue on Android where the notification trigger word modal title was "Send email notifications" instead of "Keywords".
- Fixed an issue where the Webhook icon was misaligned and bottom edges were cut off.
- Fixed an issue on Android where the user was not asked to authenticate to the app first when trying to share a photo, resulting in a white "Share modal" screen with a never-ending loading indicator.
- Fixed an issue on iOS where push notifications were not preserved when opening the app via the Mattermost icon.
## 1.15.2 Release
- Release Date: January 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue where the status changes for other users did not always stay current in the mobile app.
- Fixed an issue where a post did not fail properly when the user attempted to send the post while there was no network access.
- Fixed an issue where date separators did not update when changing timezones.
- Fixed an issue where the Favorites section did not clear from a users's channel drawer.
- Removed an extra divider below "Edit Channel" of Direct Message Channel Info.
- Fixed an issue where a user was not returned to previously viewed channel after viewing and then closing an archived channel.
- Fixed an issue where a quick double tap on switch of Channel Info created and extra on/off state.
- Fixed an issue where iOS long press menu didn't have rounded corners.
## 1.15.1 Release
- Release Date: December 28, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue preventing some users from logging in using OKTA.
## 1.15.0 Release
- Release Date: December 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Highlights
- Added mention and reply mention highlighting.
- Added a sliding animation for the reaction list.
- Added support for pinned posts.
- Added support for jumbo emojis.
- Added support for interactive dialogs.
- Improved UI for the long press menu and emoji reaction viewer.
### Improvements
- Added the ability to include custom headers with requests for custom builds.
- Push Notifications that are grouped by channels are cleared once the channel is read.
- Improved auto-reconnect when unable to reach the server.
- Added support for changing the mobile client status to offline when the app loses connection.
- Added 'View Members' button to archived channels.
- Added support on iOS for keeping the postlist in place without scrolling when new content is available.
### Bug Fixes
- Fixed an issue where clicking on a file did not show downloading progress.
- Fixed an issue on Android where on fresh install the share extension would not properly show available channels.
- Fixed an issue where recently archived channels remained in in: autocomplete when they had been archived.
- Fixed an issue where text should render when no actual custom emoji matched the named emoji pattern.
- Fixed an issue on iOS where text got cut-off after replying to a message.
- Fixed an issue where search modifier for channels was showing Direct Messages without usernames.
- Fixed an issue where "Close Channel" did not work properly when viewing two archived channels in a row.
- Fixed an issue with "Critical Error" screen when trying to upload certain file types from "+" to the left of message input box.
## 1.14.0 Release
- Release Date: November 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue where the Android app did not allow establishing a network connection with any server that used a self-signed certificate that had the CA certificate user installed on the device.
- Removed "Copy Post" option on long-press message menu for posts without text.
- Fixed an issue where the "Search Results" header was not fully scrolled to top on search "from:username".
- Fixed an issue where channel names truncated at fewer characters than necessary.
- Fixed an issue where the same uploaded photo generated a different file size.
- Fixed an issue where the "(you)" was not displayed to the right of a user's name in the channel drawer when a user opened a Direct Message channel with themself.
- Fixed an issue where a dark theme set from webapp broke mobile display.
- Fixed an issue where channel drawer transition sometimes lagged.
- Fixed an issue where sending photos to Mattermost created large files.
- Fixed an issue where the apps showed "Select a Team" screen when opened.
- Fixed an issue where at-mention, emoji, and slash command autocompletes had a double top border.
- Fixed an issue where the drawer was unable to close when showing the team list.
- Fixed an issue where team sidebar showed + sign even without more teams to join.
## 1.13.1 Release
- Release Date: October 18, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue preventing some users from authenticating using OKTA
## v1.13.0 Release
- Release Date: October 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Highlights
#### View Emoji Reactions
- Hold down on any emoji reaction to see who reacted to the post.
#### Hashtags
- Added support for searching for hashtags in posts.
#### Dropdown menus
- Added support for dropdown menus in message attachments.
### Improvements
- Added support for iPhone XR, XS and XS Max.
- Added support for nicknames on user profile.
- On servers 5.4+, added support for searching in direct and group message channels using the "in:" modifier.
- Channel autocomplete now gets closed if multiple tildes are typed.
- Added a draft icon in sidebar and channel switcher for channels with unsent messages.
- Users are now redirected to the archived channel view (rather than to Town Square) when a channel is archived.
- When closing an archived channel, users are now returned to the previously viewed channel.
### Bug Fixes
- Refactored postlist to include Android Pie fixes and smoother scrolling.
- Fixed an issue where deactivated users were not marked as such in "Jump To" search.
- Fixed an issue where users got a permission error when trying to open a file from within the image preview screen.
- Fixed an issue where session expiry notifications were not being sent on Android.
- Fixed an issue where post attachments failed to upload.
- Fixed an issue where the "DM More..." list cut off user info.
- Fixed an issue where the user would briefly see a system message when loading a reply thread.
- Fixed an issue where the error message was incorrectly formatted if the login method was set to email/password and the user tried to log in with SAML.
- Fixed an issue on Android where the keyboard sometimes overlapped the bottom of the post textbox.
- Fixed an issue where there was no option to take video via "+" > "Take Photo or Video" on iOS.
## v1.12.0 Release
- Release Date: September 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Search Date Filters
- Search for messages before, on, or after a specified date.
### Improvements
- Added notification support for Android O and P.
### Bug Fixes
- Fixed an issue where Okta was not able to login in some deployments.
- Fixed an issue where messages in Direct Message channels did not show when clicking "Jump To".
- Fixed an issue where `Show More` on a post with a message attachment displayed a blank where content should have been.
- Prevent downloading of files when disallowed in the System Console.
- Fixed an issue where users could not click on attachment filenames to open them.
- Fixed an issue where email notification settings did not save from mobile.
- Fixed an issue where the share extension allowed users to select and attempt to share content to channels that had been archived.
- Fixed an issue where reacting to an existing emoji in an archived channel was allowed.
- Fixed an issue where archived channels sometimes remained in the drawer.
- Fixed an issue where deactivated users were not marked as such in Direct Message search.
## v1.11.0 Release
- Release Date: August 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Searching Archived Channels
- Added ability to search for archived channels. Requires Mattermost server v5.2 or later.
#### Deep Linking
- Added the ability for custom builds to open Mattermost links directly in the app rather than the default mobile browser. Learn more in our [documentation](https://docs.mattermost.com/mobile/mobile-faq.html#how-do-i-configure-deep-linking)
### Improvements
- Added profile pop-up to combined system messages.
- Force re-entering SSO auth credentials after logout.
- Added consecutive posts by the same user.
- Added a loading indicator when user info is still loading in the left-hand side.
### Bug Fixes
- Fixed an issue where Android devices showed an incorrect timestamp.
- Fixed an issue on Android where the app did not get sent to the background when pressing the hardware back button in the channel screen.
- Fixed an issue with video playback when the filename had spaces.
- Fixed an issue where the app crashed when playing YouTube videos.
- Fixed an issue with session expiration notification.
- Fixed an issue with sharing files from Google Drive in Android Share Extension.
- Fixed an issue on Android where replying to a push notification sometimes went to the wrong channel.
- Fixed an issue where the previous server URL was present on the input textbox before changing the screen to Login.
- Fixed an issue where user menu was not translated correctly.
- Fixed an issue where some field lengths in Account Settings didn't match the desktop app.
- Fixed an issue where long URLs for embedded images in message attachments got cut off and didn't render.
- Fixed an issue where link preview images were not cropped properly.
- Fixed an issue where long usernames didn't wrap properly in the Account Settings menu.
- Fixed an issue where DMs would not open if users were using "Jump To".
- Fixed an issue where no message was displayed after removing a user from a channel with join/leave messages disabled.
## v1.10.0 Release
- Release Date: July 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Channel drawer performance
- Android devices will notice significant performance improvements when opening and closing the channel drawer.
#### Channel loading performance
- Improved channel loading performance as post are retrieved with every push notification
#### Announcement banner improvements
- Markdown now renders when announcement banners are expanded
- When enabled by the System Admin, users can now dismiss announcement banners until their next session
### Improvements
- Combined consecutive messages from the same user.
- Added experimental support for certificate-based authentication (CBA) for iOS to identify a user or a device before granting access to Mattermost. See [documentation](https://docs.mattermost.com/deployment/certificate-based-authentication.html) to learn more.
- Added support for the experimental automatic direct message replies feature.
- Added support for the experimental timezone feature.
- Changed post textbox to not be a connected component.
- Allow connecting to mattermost instances hosted at subpaths.
- Added support for starting YouTube videos at a given time.
- Added support for keeping messages if slash command fails.
### Bug Fixes
- Fixed an issue where the unread badge background was always white.
- Fixed an issue where a username repeated in system message if user was added to a channel more than once.
- Fixed an issue where Android Sharing from Microsoft apps failed.
- Fixed an issue where YouTube crashed the app if link did not have a time set.
- Fixed an issue where System Admins did not see all teams available to join on mobile.
- Fixed an issue where users were unable to share from Files app.
- Fixed an issue where viewing a non-existent permalink didn't show an error message.
- Fixed an issue where jumping to a channel search did not bold unread channels.
- Fixed an issue with being able to add own user to a Group Message channel.
- Fixed an issue with not being able to reply from a push notification on iOS.
- Fixed an issue where the app did not display Brazilian language.
## 1.9.3 Release
- Release Date: July 04, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed multiple issues causing app crashes
- Fixed an issue on iOS devices with typing non-english characters in the post input box
## 1.9.2 Release
- Release Date: June 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where attached videos did not play for the poster
- Fixed an issue where "Jump to recent messages" from the permalink view did not direct the user to the bottom of the channel
- Fixed an issue where post comments did not identify which parent post they belonged to
- Fixed multiple issues with typing non-english characters in the post input box
- Fixed multiple issues causing random app crashes
- Fixed an issue where files from the Android Files app failed to upload
- Fixed an issue where the iOS share extension crashed when switching the team or channel
- Fixed an issue where files from the Microsoft app failed to upload
- Fixed an issue on Android devices where sharing files changed the file extension of the attachment
## 1.9.1 Release
- Release Date: June 23, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with typing lag on Android devices
- Fixed an issue causing users to be logged out after upgrading to v1.9.0
- Fixed an issue where the ``in:`` and ``from:`` modifiers were not being added to the search field
## v1.9.0 Release
- Release Date: June 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Improved first load time on Android
- Significantly decreased first load time on Android devices from cold start.
#### iOS Files app support
- Added support for attaching files from the iOS Files app from within Mattermost.
#### Improved styling of push notification
- Improved the layout of message content, channel name and sender name in push notifications.
### Improvements
- Combined join/leave system messages.
- Added splash screen and channel loader improvements.
- Removed the desktop notification duration setting.
- Added cache team icon and set background to always be white if using a PNG file.
- Added whitelabel for icons and splash screen.
### Bug Fixes
- Fixed an issue where other user's display name did not render in combined system messages after joining the channel.
- Fixed an issue where posts incorrectly had "Commented on Someone's message" above them.
- Fixed an issue where deleting a post or its parent in permalink view left permalink view blank.
- Fixed an issue where "User is typing" message cut was off.
- Fixed an issue where `More New Messages Above` appeared at the top of new channel on joining.
- Fixed an issue where a user was not directed to Town Square when leaving a channel.
- Fixed an issue where long post were not collapsed on Android.
- Fixed an issue where a user's name was initially shown as "someone" when opening a direct message with the user.
- Fixed an issue where an error was received when trying to change the team or channel from the share extension.
- Fixed an issue where switching to a newly created channel from a push notification redirected a user to Town Square.
- Fixed an issue where a public channel made private did not disappear automatically from clients not part of the channel.
## v1.8.0 Release
- Release Date: April 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Image performance
- Images are now downloaded and stored locally for better performance
#### Flagged Posts and Recent Mentions
- Access all your flagged posts and recent mentions from the buttons in the sidebar
#### Muted Channels
- Added support for Muted Channels released with Mattermost server v4.9
### Improvements
- Date separators now appear between each posts in the search view
- Deactivated users are now filtered out of the channel members lists
- Direct Messages user list is now sorted by username first
- Added the option to Direct Message yourself from your user profile screen
- Improved performance on the post list
- Improved matching and display when searching for users in the Direct Message user list
### Bug Fixes
- Fixed an issue where emoji reactions could be added from the search view but did not appear
- Fixed an issue causing the app to crash when trying to share content from a custom keyboard
- Fixed an issue where team names were being sorted based on letter case
- Fixed an issue where username would not be inserted to the post draft when using experimental configuration settings
- Fixed an issue with nested bullet lists being cut off in the user interface
- Fixed an issue where private channels were listed in the public channels section of the channel autocomplete list
- Fixed an issue where a profile images could not be updated from the app
## v1.7.1 Release
- Release Date: April 3, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where the iOS share extension sometimes crashed the Mattermost app
- Fixed an issue preventing Markdown tables from rendering with some international characters
## v1.7.0 Release
- Release Date: March 26, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### iOS File Sharing
- Share files and images from other applications as attached files in Mattermost
#### Markdown Tables
- Tables created using markdown formatting can now be viewed in the app
#### Permalinks
- Permalinks now open in the app instead of launching a browser window
### Improvements
- Increased the tappable area of various icons for improved usability
- Announcement banners now display in the app
- Added "+" button to add emoji reactions to a post
- Minor performance improvements for app launch time
- Text files can now be viewed in the app
- Support for email autolinking into the app
### Bugs
- Fixed an issue causing some devices to hang at the splash screen on app launch
- Fixed an issue causing some letters to be hidden in the Android search input box
- Fixed an issue causing some Direct Message channels to show date stamps below the most recent message
- Fixed an issue where users weren't able to join open teams they've never been a member of
- Fixed an issue so double tapping buttons can no longer cause UI issues
- Fixed an issue where changing the channel display name wasn't being updated in the UI appropriately
- Fixed an issue where searhing for public channels sometimes showed no results
- Fixed an issue where the post menu could remain open while scrolling in the post list
- Fixed an issue where the system message to add users to a channel was missing the execution link
- Fixed an issue where bulleted lists cut off text if nested deeper than two levels
- Fixed an issue where logging into an account that is not on any team freezes the app
- Fixed an issue on iOS causing the app to crash when taking a photo then attaching it to a post
## v1.6.1 Release
- Release Date: February 13, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue preventing the app from going to the correct channel when opened from a push notification
- Fixed an issue on Android devices where the app could sometimes freeze on the launch screen
- Fixed an issue on Samsung devices causing extra letters to be insterted when typing to filter user lists
## v1.6.0 Release
- Release Date: February 6, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Android File Sharing
- Share files and images from other applications as attached files in Mattermost
### Improvements
- Added a right drawer to access settings, edit profile information, change online status and logout
- Added support for opening a Direct Message channel with yourself
### Bugs
- Fixed a number of issues causing crashes on Android devices
- Fixed an issue with auto capitalization on Android keyboards
- Fixed an issue where the GitLab SSO login button sometimes didn't appear
- Fixed an issue with link previews not appearing on some accounts
- Fixed an issue where logging out of the app didn't clear the notification badge on the homescreen icon
- Fixed an issue where interactive message buttons would not wrap to a new line
- Fixed an issue where the keyboard would sometimes overlap the text input box
- Fixed an issue where the Direct Message channel wouldn't open from the profile page
- Fixed an issue where posts would sometimes overlap
- Fixed an issue where the app sometimes hangs on logout
## v1.5.3 Release
- Release Date: February 1, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
- Fixed a login issue when connecting to servers running a Data Retention policy
## v1.5.2 Release
- Release Date: January 12, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported

View File

@@ -1,6 +1,8 @@
# Code Contribution Guidelines
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects.
Note: Community work won't start until October 31, and no community pull requests will be accepted before then.
### Review Process for this Repo

729
ImagePickerModule.java Normal file
View File

@@ -0,0 +1,729 @@
package com.imagepicker;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Patterns;
import android.webkit.MimeTypeMap;
import android.content.pm.PackageManager;
import com.facebook.react.ReactActivity;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.imagepicker.media.ImageConfig;
import com.imagepicker.permissions.PermissionUtils;
import com.imagepicker.permissions.OnImagePickerPermissionsCallback;
import com.imagepicker.utils.MediaUtils.ReadExifResult;
import com.imagepicker.utils.RealPathUtil;
import com.imagepicker.utils.UI;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.List;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.modules.core.PermissionAwareActivity;
import static com.imagepicker.utils.MediaUtils.*;
import static com.imagepicker.utils.MediaUtils.createNewFile;
import static com.imagepicker.utils.MediaUtils.getResizedImage;
public class ImagePickerModule extends ReactContextBaseJavaModule
implements ActivityEventListener
{
public static final int REQUEST_LAUNCH_IMAGE_CAPTURE = 13001;
public static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002;
public static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003;
public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004;
public static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001;
public static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002;
private final ReactApplicationContext reactContext;
private final int dialogThemeId;
protected Callback callback;
private ReadableMap options;
protected Uri cameraCaptureURI;
private Boolean noData = false;
private Boolean pickVideo = false;
private ImageConfig imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
@Deprecated
private int videoQuality = 1;
@Deprecated
private int videoDurationLimit = 0;
private ResponseHelper responseHelper = new ResponseHelper();
private PermissionListener listener = new PermissionListener()
{
public boolean onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults)
{
boolean permissionsGranted = true;
for (int i = 0; i < permissions.length; i++)
{
final boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
permissionsGranted = permissionsGranted && granted;
}
if (callback == null || options == null)
{
return false;
}
if (!permissionsGranted)
{
responseHelper.invokeError(callback, "Permissions weren't granted");
return false;
}
switch (requestCode)
{
case REQUEST_PERMISSIONS_FOR_CAMERA:
launchCamera(options, callback);
break;
case REQUEST_PERMISSIONS_FOR_LIBRARY:
launchImageLibrary(options, callback);
break;
}
return true;
}
};
public ImagePickerModule(ReactApplicationContext reactContext,
@StyleRes final int dialogThemeId)
{
super(reactContext);
this.dialogThemeId = dialogThemeId;
this.reactContext = reactContext;
this.reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "ImagePickerManager";
}
@ReactMethod
public void showImagePicker(final ReadableMap options, final Callback callback) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null)
{
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.callback = callback;
this.options = options;
imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
final AlertDialog dialog = UI.chooseDialog(this, options, new UI.OnAction()
{
@Override
public void onTakePhoto(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.launchCamera();
}
@Override
public void onUseLibrary(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.launchImageLibrary();
}
@Override
public void onCancel(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.doOnCancel();
}
@Override
public void onCustomButton(@NonNull final ImagePickerModule module,
@NonNull final String action)
{
if (module == null)
{
return;
}
module.invokeCustomButton(action);
}
});
dialog.show();
}
public void doOnCancel()
{
if (this.callback != null) {
responseHelper.invokeCancel(this.callback);
}
}
public void launchCamera()
{
this.launchCamera(this.options, this.callback);
}
// NOTE: Currently not reentrant / doesn't support concurrent requests
@ReactMethod
public void launchCamera(final ReadableMap options, final Callback callback)
{
if (!isCameraAvailable())
{
responseHelper.invokeError(callback, "Camera not available");
return;
}
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null)
{
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.callback = callback;
this.options = options;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA))
{
return;
}
parseOptions(this.options);
int requestCode;
Intent cameraIntent;
if (pickVideo)
{
requestCode = REQUEST_LAUNCH_VIDEO_CAPTURE;
cameraIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, videoQuality);
if (videoDurationLimit > 0)
{
cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, videoDurationLimit);
}
}
else
{
requestCode = REQUEST_LAUNCH_IMAGE_CAPTURE;
cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
final File original = createNewFile(reactContext, this.options, false);
imageConfig = imageConfig.withOriginalFile(original);
if (imageConfig.original != null) {
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
}else {
responseHelper.invokeError(callback, "Couldn't get file path for photo");
return;
}
if (cameraCaptureURI == null)
{
responseHelper.invokeError(callback, "Couldn't get file path for photo");
return;
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI);
}
if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null)
{
responseHelper.invokeError(callback, "Cannot launch camera");
return;
}
// Workaround for Android bug.
// grantUriPermission also needed for KITKAT,
// see https://code.google.com/p/android/issues/detail?id=76683
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
List<ResolveInfo> resInfoList = reactContext.getPackageManager().queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
reactContext.grantUriPermission(packageName, cameraCaptureURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
try
{
currentActivity.startActivityForResult(cameraIntent, requestCode);
}
catch (ActivityNotFoundException e)
{
e.printStackTrace();
responseHelper.invokeError(callback, "Cannot launch camera");
}
}
public void launchImageLibrary()
{
this.launchImageLibrary(this.options, this.callback);
}
// NOTE: Currently not reentrant / doesn't support concurrent requests
@ReactMethod
public void launchImageLibrary(final ReadableMap options, final Callback callback)
{
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.options = options;
this.callback = callback;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY))
{
return;
}
parseOptions(this.options);
int requestCode;
Intent libraryIntent;
if (pickVideo)
{
requestCode = REQUEST_LAUNCH_VIDEO_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK);
libraryIntent.setType("video/*");
}
else
{
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
{
responseHelper.invokeError(callback, "Cannot launch photo library");
return;
}
try
{
currentActivity.startActivityForResult(libraryIntent, requestCode);
}
catch (ActivityNotFoundException e)
{
e.printStackTrace();
responseHelper.invokeError(callback, "Cannot launch photo library");
}
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
//robustness code
if (passResult(requestCode))
{
return;
}
responseHelper.cleanResponse();
// user cancel
if (resultCode != Activity.RESULT_OK)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.invokeCancel(callback);
callback = null;
return;
}
Uri uri = null;
switch (requestCode)
{
case REQUEST_LAUNCH_IMAGE_CAPTURE:
uri = cameraCaptureURI;
break;
case REQUEST_LAUNCH_IMAGE_LIBRARY:
uri = data.getData();
String realPath = getRealPathFromURI(uri);
final boolean isUrl = !TextUtils.isEmpty(realPath) &&
Patterns.WEB_URL.matcher(realPath).matches();
if (realPath == null || isUrl)
{
try
{
File file = createFileFromURI(uri);
realPath = file.getAbsolutePath();
uri = Uri.fromFile(file);
}
catch (Exception e)
{
// image not in cache
responseHelper.putString("error", "Could not read photo");
responseHelper.putString("uri", uri.toString());
responseHelper.invokeResponse(callback);
callback = null;
return;
}
}
imageConfig = imageConfig.withOriginalFile(new File(realPath));
break;
case REQUEST_LAUNCH_VIDEO_LIBRARY:
responseHelper.putString("uri", data.getData().toString());
responseHelper.putString("path", getRealPathFromURI(data.getData()));
responseHelper.invokeResponse(callback);
callback = null;
return;
case REQUEST_LAUNCH_VIDEO_CAPTURE:
final String path = getRealPathFromURI(data.getData());
responseHelper.putString("uri", data.getData().toString());
responseHelper.putString("path", path);
fileScan(reactContext, path);
responseHelper.invokeResponse(callback);
callback = null;
return;
}
final ReadExifResult result = readExifInterface(responseHelper, imageConfig);
if (result.error != null)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.invokeError(callback, result.error.getMessage());
callback = null;
return;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imageConfig.original.getAbsolutePath(), options);
int initialWidth = options.outWidth;
int initialHeight = options.outHeight;
updatedResultResponse(uri, imageConfig.original.getAbsolutePath());
// don't create a new file if contraint are respected
if (imageConfig.useOriginal(initialWidth, initialHeight, result.currentRotation))
{
responseHelper.putInt("width", initialWidth);
responseHelper.putInt("height", initialHeight);
fileScan(reactContext, imageConfig.original.getAbsolutePath());
}
else
{
imageConfig = getResizedImage(reactContext, this.options, imageConfig, initialWidth, initialHeight, requestCode);
if (imageConfig.resized == null)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.putString("error", "Can't resize the image");
}
else
{
uri = Uri.fromFile(imageConfig.resized);
BitmapFactory.decodeFile(imageConfig.resized.getAbsolutePath(), options);
responseHelper.putInt("width", options.outWidth);
responseHelper.putInt("height", options.outHeight);
updatedResultResponse(uri, imageConfig.resized.getAbsolutePath());
fileScan(reactContext, imageConfig.resized.getAbsolutePath());
}
}
if (imageConfig.saveToCameraRoll && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
{
final RolloutPhotoResult rolloutResult = rolloutPhotoFromCamera(imageConfig);
if (rolloutResult.error == null)
{
imageConfig = rolloutResult.imageConfig;
uri = Uri.fromFile(imageConfig.getActualFile());
updatedResultResponse(uri, imageConfig.getActualFile().getAbsolutePath());
}
else
{
removeUselessFiles(requestCode, imageConfig);
final String errorMessage = new StringBuilder("Error moving image to camera roll: ")
.append(rolloutResult.error.getMessage()).toString();
responseHelper.putString("error", errorMessage);
return;
}
}
responseHelper.invokeResponse(callback);
callback = null;
this.options = null;
}
public void invokeCustomButton(@NonNull final String action)
{
responseHelper.invokeCustomButton(this.callback, action);
}
@Override
public void onNewIntent(Intent intent) { }
public Context getContext()
{
return getReactApplicationContext();
}
public @StyleRes int getDialogThemeId()
{
return this.dialogThemeId;
}
public @NonNull Activity getActivity()
{
return getCurrentActivity();
}
private boolean passResult(int requestCode)
{
return callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|| (requestCode != REQUEST_LAUNCH_IMAGE_CAPTURE && requestCode != REQUEST_LAUNCH_IMAGE_LIBRARY
&& requestCode != REQUEST_LAUNCH_VIDEO_LIBRARY && requestCode != REQUEST_LAUNCH_VIDEO_CAPTURE);
}
private void updatedResultResponse(@Nullable final Uri uri,
@NonNull final String path)
{
responseHelper.putString("uri", uri.toString());
responseHelper.putString("path", path);
if (!noData) {
responseHelper.putString("data", getBase64StringFromFile(path));
}
putExtraFileInfo(path, responseHelper);
}
private boolean permissionsCheck(@NonNull final Activity activity,
@NonNull final Callback callback,
@NonNull final int requestCode)
{
final int writePermission = ActivityCompat
.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
final int cameraPermission = ActivityCompat
.checkSelfPermission(activity, Manifest.permission.CAMERA);
final boolean permissionsGrated = writePermission == PackageManager.PERMISSION_GRANTED &&
cameraPermission == PackageManager.PERMISSION_GRANTED;
if (!permissionsGrated)
{
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
if (dontAskAgain)
{
final AlertDialog dialog = PermissionUtils
.explainingDialog(this, options, new PermissionUtils.OnExplainingPermissionCallback()
{
@Override
public void onCancel(WeakReference<ImagePickerModule> moduleInstance,
DialogInterface dialogInterface)
{
final ImagePickerModule module = moduleInstance.get();
if (module == null)
{
return;
}
module.doOnCancel();
}
@Override
public void onReTry(WeakReference<ImagePickerModule> moduleInstance,
DialogInterface dialogInterface)
{
final ImagePickerModule module = moduleInstance.get();
if (module == null)
{
return;
}
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", module.getContext().getPackageName(), null);
intent.setData(uri);
final Activity innerActivity = module.getActivity();
if (innerActivity == null)
{
return;
}
innerActivity.startActivityForResult(intent, 1);
}
});
if (dialog != null) {
dialog.show();
}
return false;
}
else
{
String[] PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
if (activity instanceof ReactActivity)
{
((ReactActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
}
else if (activity instanceof OnImagePickerPermissionsCallback)
{
((OnImagePickerPermissionsCallback) activity).setPermissionListener(listener);
ActivityCompat.requestPermissions(activity, PERMISSIONS, requestCode);
}
else if (activity instanceof PermissionAwareActivity) {
((PermissionAwareActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
}
else
{
final String errorDescription = new StringBuilder(activity.getClass().getSimpleName())
.append(" must implement ")
.append(OnImagePickerPermissionsCallback.class.getSimpleName())
.append(PermissionAwareActivity.class.getSimpleName())
.toString();
throw new UnsupportedOperationException(errorDescription);
}
return false;
}
}
return true;
}
private boolean isCameraAvailable() {
return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
private @NonNull String getRealPathFromURI(@NonNull final Uri uri) {
return RealPathUtil.getRealPathFromURI(reactContext, uri);
}
/**
* Create a file from uri to allow image picking of image in disk cache
* (Exemple: facebook image, google image etc..)
*
* @doc =>
* https://github.com/nostra13/Android-Universal-Image-Loader#load--display-task-flow
*
* @param uri
* @return File
* @throws Exception
*/
private File createFileFromURI(Uri uri) throws Exception {
File file = new File(reactContext.getExternalCacheDir(), "photo-" + uri.getLastPathSegment());
InputStream input = reactContext.getContentResolver().openInputStream(uri);
OutputStream output = new FileOutputStream(file);
try {
byte[] buffer = new byte[4 * 1024];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.flush();
} finally {
output.close();
input.close();
}
return file;
}
private String getBase64StringFromFile(String absoluteFilePath) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(new File(absoluteFilePath));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
byte[] bytes;
byte[] buffer = new byte[8192];
int bytesRead;
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
bytes = output.toByteArray();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
private void putExtraFileInfo(@NonNull final String path,
@NonNull final ResponseHelper responseHelper)
{
// size && filename
try {
File f = new File(path);
responseHelper.putDouble("fileSize", f.length());
responseHelper.putString("fileName", f.getName());
} catch (Exception e) {
e.printStackTrace();
}
// type
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
if (extension != null) {
responseHelper.putString("type", MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension));
}
}
private void parseOptions(final ReadableMap options) {
noData = false;
if (options.hasKey("noData")) {
noData = options.getBoolean("noData");
}
imageConfig = imageConfig.updateFromOptions(options);
pickVideo = false;
if (options.hasKey("mediaType") && options.getString("mediaType").equals("video")) {
pickVideo = true;
}
videoQuality = 1;
if (options.hasKey("videoQuality") && options.getString("videoQuality").equals("low")) {
videoQuality = 0;
}
videoDurationLimit = 0;
if (options.hasKey("durationLimit")) {
videoDurationLimit = options.getInt("durationLimit");
}
}
}

View File

@@ -1,4 +1,4 @@
Copyright 2015-present Mattermost, Inc.
Copyright 2016 Mattermost, Inc.
Apache License
Version 2.0, January 2004

219
Makefile
View File

@@ -1,34 +1,25 @@
.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)
POD := $(shell command -v 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
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
.yarninstall: package.json
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
@echo "yarn is not installed https://yarnpkg.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm install
@yarn install --pure-lockfile
npm-ci: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm ci
@touch $@
.podinstall:
ifeq ($(OS), Darwin)
@@ -52,37 +43,30 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
pre-run: | .yarninstall .podinstall dist/assets ## 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: .yarninstall ## Runs eslint
@echo Checking for style guide compliance
@npm run check
@node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .
clean: ## Cleans dependencies, previous builds and temp files
@echo Cleaning started
@yarn cache clean
@rm -rf node_modules
@rm -f .yarninstall
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@rm -rf ios/Pods
@rm -rf android/app/build
@echo Cleanup finished
post-install:
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
@# Need to copy custom RNCWebViewManager.java and RNCWEKWebView.m that implements IWA support for the WebView to avoid forking the lib
@cp ./native_modules/RNCWebViewManager.java node_modules/react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
@cp ./native_modules/RNCWKWebView.m node_modules/react-native-webview/ios/RNCWKWebView.m
# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
@./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
@# Must remove the .babelrc for 0.42.0 to work correctly
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
@cp ./ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@@ -90,45 +74,60 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
@sed -i'' -e "s|super.onBackPressed();|this.moveTaskToBack(true);|g" node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/controllers/NavigationActivity.java
@sed -i'' -e "s|compile 'com.facebook.react:react-native:0.17.+'|compile 'com.facebook.react:react-native:+'|g" node_modules/react-native-bottom-sheet/android/build.gradle
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier QuartzBookPack && yarn run postinstall
@cd ./node_modules/mattermost-redux && yarn 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; \
node ./node_modules/react-native/local-cli/cli.js start; \
else \
echo React Native packager server already running; \
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \
fi
stop: ## Stops the React Native packager server
$(call stop_packager)
@echo Stopping React Native packager server
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | 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 \
@if ! [ $(shell command -v xcodebuild) ]; then \
echo "xcode is not installed"; \
exit 1; \
fi
@if ! [ $(shell which watchman) ]; then \
@if ! [ $(shell command -v watchman) ]; then \
echo "watchman is not installed"; \
exit 1; \
fi
check-device-android:
@if ! [ $(ANDROID_HOME) ]; then \
echo "ANDROID_HOME is not set"; \
exit 1; \
@echo "ANDROID_HOME is not set"; \
@exit 1; \
fi
@if ! [ $(shell which adb 2> /dev/null) ]; then \
echo "adb is not installed"; \
exit 1; \
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
@echo "adb is not installed"; \
@exit 1; \
fi
@echo "Connect your Android device or open the emulator"
@adb wait-for-device
@if ! [ $(shell which watchman 2> /dev/null) ]; then \
echo "watchman is not installed"; \
exit 1; \
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
fi
prepare-android-build:
@@ -139,28 +138,20 @@ 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 \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
node ./node_modules/react-native/local-cli/cli.js start & echo Running iOS app in development; \
react-native run-ios --simulator="${SIMULATOR}"; \
wait; \
else \
echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
react-native run-ios --simulator="${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; \
node ./node_modules/react-native/local-cli/cli.js start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
@@ -176,93 +167,61 @@ 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
ifneq ($(IOS_APP_GROUP),)
@mkdir -p assets/override
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
endif
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & 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
@rm -rf assets/override
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; \
node ./node_modules/react-native/local-cli/cli.js 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; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
@echo "Building unsigned iOS app"
ifneq ($(IOS_APP_GROUP),)
@mkdir -p assets/override
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
endif
@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)
@rm -rf assets/override
@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; \
node ./node_modules/react-native/local-cli/cli.js 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
@yarn test
## 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

3800
NOTICE.txt

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
Please make sure you've read the [pull request](https://developers.mattermost.com/contribute/getting-started/contribution-checklist/) section of our [code contribution guidelines](https://developers.mattermost.com/contribute/getting-started/).
Please make sure you've read the [pull request](http://docs.mattermost.com/developer/contribution-guide.html#preparing-a-pull-request) section of our [code contribution guidelines](http://docs.mattermost.com/developer/contribution-guide.html).
When filling in a section please remove the help text and the above text.

View File

@@ -1,17 +1,16 @@
# Mattermost Mobile
- **Supported Server versions:** 4.10+
- **Supported iOS versions:** 10.3+
- **Supported Android versions:** 7.0+
**Supported Server Versions:** 4.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at [https://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.
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
# How to Contribute
### Testing
@@ -19,22 +18,22 @@ We plan on releasing monthly updates with new features - check the [changelog](h
To help with testing app updates before they're released, you can:
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://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.
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
2. Install the `Mattermost Beta` app
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
- Device information
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
- Device information
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
### Contribute Code
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues?q=label%3A"React+Native") for issues marked as [Help Wanted]
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-mobile/issues) for issues marked as [Help Wanted]
2. Comment to let people know youre 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 +44,13 @@ To help with testing app updates before they're released, you can:
App data is wiped from the device when a user logs out of the app. If the user is logged in when the account is deactivated, then within one minute the system logs the user out, and as a result all app data is wiped from the device.
### Can I connect to multiple Mattermost servers using the mobile apps?
### Can I connect to multiple Mattermost servers using the mobile apps?**
At the moment, we only support connecting to one server at a time. If you need to connect to multiple servers, please [upvote the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/10975938) so we can track demand for it.
As a work around, you can install both the released "Mattermost" app and sign up to be a [tester](#testing) for the "Mattermost Beta" app so you can connect to two servers at once.
### Will there be second generation apps available for tablets?
### Will there be second generation apps available for tablets?**
We plan to add support for tablets in the future, but the timeline depends on how many people have a need for it. If you're looking for a tablet version, please help us out by [upvoting the feature request](https://mattermost.uservoice.com/forums/306457/suggestions/20082079)!

View File

@@ -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(

View File

@@ -73,20 +73,13 @@ import com.android.build.OutputFile
*/
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "packager-config.js"
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
project.ext.sentryCli = [
logLevel: "error",
flavorAware: false
]
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
@@ -106,21 +99,16 @@ 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 187
versionName "1.18.1"
multiDexEnabled = true
minSdkVersion 21
targetSdkVersion 23
versionCode 86
versionName "1.6.1"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -157,7 +145,6 @@ android {
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
matchingFallbacks = ['debug', 'release']
}
}
// applicationVariants are e.g. debug, release
@@ -175,70 +162,39 @@ android {
}
}
repositories {
maven {
url 'https://maven.google.com'
}
}
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'
}
}
}
}
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 project(':react-native-doc-viewer')
compile project(':react-native-video')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile 'com.android.support:percent:25.3.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')
compile project(':react-native-orientation')
compile project(':react-native-bottom-sheet')
compile ('com.google.android.gms:play-services-gcm:9.4.0') {
force = true;
}
compile project(':react-native-device-info')
compile project(':reactnativenotifications')
compile project(':react-native-cookies')
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-local-auth')
compile project(':jail-monkey')
compile project(':react-native-youtube')
compile project(':react-native-sentry')
compile project(':react-native-exception-handler')
compile project(':react-native-fetch-blob')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
compile 'com.facebook.fresco:animated-base-support:1.3.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
compile 'com.facebook.fresco:animated-gif:1.3.0'
compile 'com.facebook.fresco:animated-webp:1.3.0'
compile 'com.facebook.fresco:webpsupport:1.3.0'
}
// Run this once to be able to run the application with BUCK
@@ -247,5 +203,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

View File

@@ -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.**

View File

@@ -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" />
@@ -37,13 +50,14 @@
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />
<receiver android:name=".NotificationReplyBroadcastReceiver"
<service android:name=".NotificationReplyService"
android:enabled="true"
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:noHistory="false"
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
@@ -53,7 +67,7 @@
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- for sharing-->
// for sharing
<data android:mimeType="*/*" />
</intent-filter>
</activity>

View File

@@ -0,0 +1,30 @@
package com.mattermost.components;
import android.content.Context;
import android.text.InputType;
import com.facebook.react.views.textinput.ReactEditText;
public class CustomTextInput extends ReactEditText {
private boolean autoScroll = false;
public CustomTextInput(Context context) {
super(context);
}
private boolean isMultiline() {
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
@Override
public boolean isLayoutRequested() {
if (isMultiline() && !autoScroll) {
return true;
}
return false;
}
public void setAutoScroll(boolean autoScroll) {
this.autoScroll = autoScroll;
}
}

View File

@@ -0,0 +1,35 @@
package com.mattermost.components;
import android.text.InputType;
import android.util.TypedValue;
import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.annotations.ReactProp;
public class CustomTextInputManager extends ReactTextInputManager {
@Override
public String getName() {
return "CustomTextInput";
}
@Override
public CustomTextInput createViewInstance(ThemedReactContext context) {
CustomTextInput editText = new CustomTextInput(context);
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
return editText;
}
@ReactProp(name = "autoScroll", defaultBoolean = false)
public void setAutoScroll(CustomTextInput view, boolean autoScroll) {
view.setAutoScroll(autoScroll);
}
}

View File

@@ -1,93 +0,0 @@
package com.mattermost.react_native_interface;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.modules.storage.ReactDatabaseSupplier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
/**
* AsyncStorageHelper: Class that accesses React Native AsyncStorage Database synchronously
*/
public class AsyncStorageHelper {
// Static variables from: com.facebook.react.modules.storage.ReactDatabaseSupplier
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
private static final int MAX_SQL_KEYS = 999;
Context mReactContext = null;
public AsyncStorageHelper(Context mReactContext) {
this.mReactContext = mReactContext;
}
public HashMap<String, String> multiGet(ReadableArray keys) {
HashMap<String, String> results = new HashMap<>(keys.size());
HashSet<String> keysRemaining = new HashSet<>();
String[] columns = {KEY_COLUMN, VALUE_COLUMN};
ReactDatabaseSupplier reactDatabaseSupplier = ReactDatabaseSupplier.getInstance(this.mReactContext);
for (int keyStart = 0; keyStart < keys.size(); keyStart += MAX_SQL_KEYS) {
int keyCount = Math.min(keys.size() - keyStart, MAX_SQL_KEYS);
Cursor cursor = reactDatabaseSupplier.get().query(
TABLE_CATALYST,
columns,
buildKeySelection(keyCount),
buildKeySelectionArgs(keys, keyStart, keyCount),
null,
null,
null);
keysRemaining.clear();
try {
if (cursor.getCount() != keys.size()) {
// some keys have not been found - insert them with null into the final array
for (int keyIndex = keyStart; keyIndex < keyStart + keyCount; keyIndex++) {
keysRemaining.add(keys.getString(keyIndex));
}
}
if (cursor.moveToFirst()) {
do {
results.put(cursor.getString(0), cursor.getString(1));
keysRemaining.remove(cursor.getString(0));
} while (cursor.moveToNext());
}
} catch (Exception e) {
return new HashMap<>(1);
} finally {
cursor.close();
}
for (String key : keysRemaining) {
results.put(key, null);
}
keysRemaining.clear();
}
return results;
}
private static String buildKeySelection(int selectionCount) {
String[] list = new String[selectionCount];
Arrays.fill(list, "?");
return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
}
private static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) {
String[] selectionArgs = new String[count];
for (int keyIndex = 0; keyIndex < count; keyIndex++) {
selectionArgs[keyIndex] = keys.getString(start + keyIndex);
}
return selectionArgs;
}
}

View File

@@ -1,68 +0,0 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import java.util.ArrayList;
/**
* KeysReadableArray: Helper class that abstracts boilerplate
*/
public class KeysReadableArray implements ReadableArray {
@Override
public int size() {
return 0;
}
@Override
public boolean isNull(int index) {
return false;
}
@Override
public boolean getBoolean(int index) {
return false;
}
@Override
public double getDouble(int index) {
return 0;
}
@Override
public int getInt(int index) {
return 0;
}
@Override
public String getString(int index) {
return null;
}
@Override
public ReadableArray getArray(int index) {
return null;
}
@Override
public ReadableMap getMap(int index) {
return null;
}
@Override
public Dynamic getDynamic(int index) {
return null;
}
@Override
public ReadableType getType(int index) {
return null;
}
@Override
public ArrayList<Object> toArrayList() {
return null;
}
}

View File

@@ -1,64 +0,0 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
/**
* ResolvePromise: Helper class that abstracts boilerplate
*/
public class ResolvePromise implements Promise {
@Override
public void resolve(@javax.annotation.Nullable Object value) {
}
@Override
public void reject(String code, String message) {
}
@Override
public void reject(String code, WritableMap map) {
}
@Override
public void reject(String code, Throwable e) {
}
@Override
public void reject(Throwable e, WritableMap map) {
}
@Override
public void reject(String code, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e) {
}
@Override
public void reject(String code, String message, WritableMap map) {
}
@Override
public void reject(String message) {
}
@Override
public void reject(Throwable reason) {
}
}

View File

@@ -1,14 +1,10 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
@@ -17,14 +13,15 @@ import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.provider.Settings.System;
import android.util.Log;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
@@ -33,9 +30,12 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
import android.util.Log;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification {
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
public static final String NOTIFICATION_ID = "notificationId";
@@ -46,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);
}
}
}
@@ -112,11 +97,6 @@ public class CustomPushNotification extends PushNotification {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
if (!"clear".equals(type)) {
String senderName = getSenderName(data.getString("channel_name"), data.getString("message"));
data.putLong("time", new Date().getTime());
data.putString("sender_name", senderName);
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
}
@@ -138,6 +118,7 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
digestNotification();
clearAllNotifications();
}
@Override
@@ -161,50 +142,26 @@ public class CustomPushNotification extends PushNotification {
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
channel.setShowBadge(true);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification.setChannelId(CHANNEL_ID);
}
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
String title = null;
if (version != null && version.equals("v2")) {
title = bundle.getString("channel_name");
} else {
title = bundle.getString("title");
}
if (android.text.TextUtils.isEmpty(title)) {
String title = bundle.getString("title");
if (title == null) {
ApplicationInfo appInfo = mContext.getApplicationInfo();
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
String badge = bundle.getString("badge");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
String message = bundle.getString("message");
String subText = bundle.getString("subText");
String numberString = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
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;
@@ -229,49 +186,49 @@ public class CustomPushNotification extends PushNotification {
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
}
if (badge != null) {
int badgeCount = Integer.parseInt(badge);
CustomPushNotification.badgeCount = badgeCount;
notification.setNumber(badgeCount);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
} else {
// HERE ADD THE DOT INDICATOR STUFF
if (numberString != null) {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("You");
if (title != null && !title.startsWith("@")) {
messagingStyle
.setConversationTitle(title);
}
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
list.add(bundle);
}
for (Bundle data : list) {
String message = data.getString("message");
if (title == null || !title.startsWith("@")) {
message = removeSenderFromMessage(message);
}
messagingStyle.addMessage(message, data.getLong("time"), data.getString("sender_name"));
}
int numMessages = getMessageCountInChannel(channelId);
notification
.setContentIntent(intent)
.setGroupSummary(true)
.setStyle(messagingStyle)
.setSmallIcon(smallIconResId)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setBadgeIconType(Notification.BADGE_ICON_SMALL);
if (numMessages == 1) {
notification
.setContentTitle(title)
.setContentText(message)
.setStyle(new Notification.BigTextStyle()
.bigText(message));
} else {
String summaryTitle = String.format("%s (%d)", title, numMessages);
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
}
for (Bundle data : list) {
String msg = data.getString("message");
if (msg != message) {
style.addLine(data.getString("message"));
}
}
style.setBigContentTitle(message)
.setSummaryText(String.format("+%d more", (numMessages - 1)));
notification.setStyle(style)
.setContentTitle(summaryTitle);
}
// Let's add a delete intent when the notification is dismissed
@@ -285,11 +242,11 @@ public class CustomPushNotification extends PushNotification {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
Intent replyIntent = new Intent(mContext, NotificationReplyService.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
@@ -311,6 +268,10 @@ public class CustomPushNotification extends PushNotification {
notification.setLargeIcon(largeIconBitmap);
}
if (subText != null) {
notification.setSubText(subText);
}
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (soundUri != "none") {
@@ -350,29 +311,15 @@ public class CustomPushNotification extends PushNotification {
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");
final String numberString = data.getString("badge");
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
}
private String getSenderName(String channelName, String message) {
if (channelName != null && channelName.startsWith("@")) {
return channelName;
String numberString = data.getString("badge");
if (numberString != null) {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
String senderName = message.split(":")[0];
if (senderName != message) {
return senderName;
}
return " ";
}
private String removeSenderFromMessage(String message) {
String sender = String.format("%s: ", getSenderName("", message));
return message.replaceFirst(sender, "");
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}

View File

@@ -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();
}
}

View File

@@ -1,146 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.support.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
import com.mattermost.react_native_interface.ResolvePromise;
import com.oblador.keychain.KeychainModule;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class InitializationModule extends ReactContextBaseJavaModule {
static final String TOOLBAR_BACKGROUND = "TOOLBAR_BACKGROUND";
static final String TOOLBAR_TEXT_COLOR = "TOOLBAR_TEXT_COLOR";
static final String APP_BACKGROUND = "APP_BACKGROUND";
private final Application mApplication;
public InitializationModule(Application application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
@Override
public String getName() {
return "Initialization";
}
@Nullable
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
/**
* Package all native module variables in constants
* in order to avoid the native bridge
*
* KeyStore:
* credentialsExist
* deviceToken
* currentUserId
* token
* url
*
* AsyncStorage:
* toolbarBackground
* toolbarTextColor
* appBackground
*
* Miscellaneous:
* MattermostManaged.Config
* replyFromPushNotification
*/
MainApplication app = (MainApplication) mApplication;
final Boolean[] credentialsExist = {false};
final WritableMap[] credentials = {null};
final Object[] config = {null};
// Get KeyStore credentials
KeychainModule module = new KeychainModule(this.getReactApplicationContext());
module.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
credentialsExist[0] = false;
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
credentialsExist[0] = true;
credentials[0] = map;
}
}
});
// Get managedConfig from MattermostManagedModule
MattermostManagedModule.getInstance().getConfig(new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
WritableNativeMap nativeMap = (WritableNativeMap) value;
config[0] = value;
}
});
// Get AsyncStorage key/values
final ArrayList<String> keys = new ArrayList<String>(5);
keys.add(TOOLBAR_BACKGROUND);
keys.add(TOOLBAR_TEXT_COLOR);
keys.add(APP_BACKGROUND);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
AsyncStorageHelper asyncStorage = new AsyncStorageHelper(this.getReactApplicationContext());
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String toolbarBackground = asyncStorageResults.get(TOOLBAR_BACKGROUND);
String toolbarTextColor = asyncStorageResults.get(TOOLBAR_TEXT_COLOR);
String appBackground = asyncStorageResults.get(APP_BACKGROUND);
if (toolbarBackground != null
&& toolbarTextColor != null
&& appBackground != null) {
constants.put("themesExist", true);
constants.put("toolbarBackground", toolbarBackground);
constants.put("toolbarTextColor", toolbarTextColor);
constants.put("appBackground", appBackground);
} else {
constants.put("themesExist", false);
}
if (credentialsExist[0]) {
constants.put("credentialsExist", true);
constants.put("credentials", credentials[0]);
} else {
constants.put("credentialsExist", false);
}
constants.put("managedConfig", config[0]);
constants.put("replyFromPushNotification", app.replyFromPushNotification);
app.replyFromPushNotification = false;
return constants;
}
}

View File

@@ -1,36 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class InitializationPackage implements ReactPackage {
private final Application mApplication;
public InitializationPackage(Application application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new InitializationModule(mApplication, reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -1,18 +1,12 @@
package com.mattermost.rnbeta;
import com.mattermost.share.SharePackage;
import com.mattermost.share.RealPathUtil;
import android.app.Activity;
import android.app.Application;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.oblador.keychain.KeychainPackage;
import com.facebook.react.ReactApplication;
import com.reactlibrary.RNReactNativeDocViewerPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.horcrux.svg.SvgPackage;
@@ -22,11 +16,10 @@ 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.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.imagepicker.ImagePickerPackage;
@@ -35,22 +28,20 @@ import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import 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;
@Override
public boolean isDebug() {
@@ -70,23 +61,18 @@ public class MainApplication extends NavigationApplication implements INotificat
new VectorIconsPackage(),
new SvgPackage(),
new LinearGradientPackage(),
new OrientationPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
new RNFetchBlobPackage(),
new MattermostPackage(this),
new RNSentryPackage(),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
new RNReactNativeDocViewerPackage(),
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this),
new RNRecyclerviewListPackage(),
new RNCWebViewPackage(),
new RNGestureHandlerPackage()
new SharePackage()
);
}
@@ -99,12 +85,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 +94,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 +110,4 @@ public class MainApplication extends NavigationApplication implements INotificat
new JsIOHelper()
);
}
@Override
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
}

View File

@@ -10,6 +10,8 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
import com.mattermost.components.CustomTextInputManager;
public class MattermostPackage implements ReactPackage {
private final MainApplication mApplication;
@@ -27,6 +29,8 @@ public class MattermostPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList();
return Arrays.<ViewManager>asList(
new CustomTextInputManager()
);
}
}

View File

@@ -11,7 +11,7 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
super("notificationDismissService");
}
@Override

View File

@@ -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);
}
}

View File

@@ -1,155 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import com.mattermost.react_native_interface.ResolvePromise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.oblador.keychain.KeychainModule;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
private Context mContext;
private Bundle bundle;
private NotificationManager notificationManager;
@Override
public void onReceive(Context context, Intent intent) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
mContext = context;
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
final CharSequence message = getReplyMessage(intent);
final KeychainModule keychainModule = new KeychainModule(reactApplicationContext);
keychainModule.getGenericPasswordForOptions(null, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
String channelId = bundle.getString("channel_id");
onReplyFailed(notificationManager, notificationId, channelId);
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String[] credentials = map.getString("password").split(",[ ]*");
String token = null;
String serverUrl = null;
if (credentials.length == 2) {
token = credentials[0];
serverUrl = credentials[1];
}
Log.i("ReactNative", String.format("URL=%s TOKEN=%s", serverUrl, token));
replyToMessage(serverUrl, token, notificationId, message);
}
}
});
}
}
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
final String channelId = bundle.getString("channel_id");
final String rootId = bundle.getString("post_id");
if (token == null || serverUrl == null) {
onReplyFailed(notificationManager, notificationId, channelId);
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildReplyPost(channelId, rootId, message.toString());
Log.i("ReactNative", String.format("JSON STRING %s", json));
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(String.format("%s/api/v4/posts", serverUrl.replaceAll("/$", "")))
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.i("ReactNative", String.format("Reply with message %s FAILED exception %s", message, e.getMessage()));
onReplyFailed(notificationManager, notificationId, channelId);
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationManager, notificationId, channelId);
Log.i("ReactNative", String.format("Reply with message %s", message));
} else {
Log.i("ReactNative", String.format("Reply with message %s FAILED status %s BODY %s", message, response.code(), response.body().string()));
onReplyFailed(notificationManager, notificationId, channelId);
}
}
});
}
protected String buildReplyPost(String channelId, String rootId, String message) {
return "{"
+ "\"channel_id\": \"" + channelId + "\","
+ "\"message\": \"" + message + "\","
+ "\"root_id\": \"" + rootId + "\""
+ "}";
}
protected void onReplyFailed(NotificationManager notificationManager, int notificationId, String channelId) {
String CHANNEL_ID = "Reply job";
Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Message failed to send.")
.setSmallIcon(smallIconResId)
.build();
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
notificationManager.notify(notificationId, notification);
}
protected void onReplySuccess(NotificationManager notificationManager, int notificationId, String channelId) {
notificationManager.cancel(notificationId);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

View File

@@ -0,0 +1,53 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
String channelId = bundle.getString("channel_id");
bundle.putCharSequence("text", message);
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
Log.i("ReactNative", "Replying service");
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),
5000);
}
return null;
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

View File

@@ -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

View File

@@ -6,13 +6,9 @@ import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.util.Log;
import android.text.TextUtils;
import android.os.ParcelFileDescriptor;
import java.io.*;
@@ -40,17 +36,10 @@ public class RealPathUtil {
// DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
try {
return getPathFromSavingTempFile(context, uri);
} catch (NumberFormatException e) {
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
return null;
}
}
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) {
// MediaProvider
@@ -74,15 +63,19 @@ public class RealPathUtil {
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
if ("content".equalsIgnoreCase(uri.getScheme())) {
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// MediaStore (and general)
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
String path = getDataColumn(context, uri, null, null);
if (path != null) {
return path;
}
// Try save to tmp file, and return tmp file path
return getPathFromSavingTempFile(context, uri);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
@@ -94,33 +87,9 @@ 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();
}
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
String fileName = uri.getLastPathSegment();
tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
@@ -203,31 +172,4 @@ public class RealPathUtil {
File file = new File(filePath);
return getMimeType(file);
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
try {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
} catch (Exception e) {
return "application/octet-stream";
}
}
public static void deleteTempFiles(final File dir) {
try {
if (dir.isDirectory()) {
deleteRecursive(dir);
}
} catch (Exception e) {
// do nothing
}
}
private static void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory())
for (File child : fileOrDirectory.listFiles())
deleteRecursive(child);
fileOrDirectory.delete();
}
}

View File

@@ -1,20 +1,13 @@
package com.mattermost.share;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
import com.mattermost.rnbeta.MainApplication;
public class ShareActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "MattermostShare";
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainApplication app = (MainApplication) this.getApplication();
app.sharedExtensionIsOpened = true;
}
}

View File

@@ -2,6 +2,7 @@ package com.mattermost.share;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
@@ -9,25 +10,23 @@ import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.Arguments;
import com.mattermost.rnbeta.MainApplication;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.graphics.Bitmap;
import java.io.InputStream;
import java.io.File;
import java.util.ArrayList;
import javax.annotation.Nullable;
import javax.annotation.Nonnull;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -38,13 +37,9 @@ import okhttp3.Response;
public class ShareModule extends ReactContextBaseJavaModule {
private final OkHttpClient client = new OkHttpClient();
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private final MainApplication mApplication;
public ShareModule(MainApplication application, ReactApplicationContext reactContext) {
public ShareModule(ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
private File tempFolder;
@Override
public String getName() {
@@ -63,21 +58,11 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
}
@Nullable
@Override
public Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<>(1);
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
mApplication.sharedExtensionIsOpened = false;
return constants;
}
@ReactMethod
public void close(ReadableMap data) {
this.clear();
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");
@@ -93,8 +78,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
}
}
RealPathUtil.deleteTempFiles(this.tempFolder);
}
@ReactMethod
@@ -102,23 +85,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
promise.resolve(processIntent());
}
@ReactMethod
public void getFilePath(String filePath, Promise promise) {
Activity currentActivity = getCurrentActivity();
WritableMap map = Arguments.createMap();
if (currentActivity != null) {
Uri uri = Uri.parse(filePath);
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
if (path != null) {
String text = "file://" + path;
map.putString("filePath", text);
}
}
promise.resolve(map);
}
public WritableArray processIntent() {
WritableMap map = Arguments.createMap();
WritableArray items = Arguments.createArray();
@@ -130,7 +96,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
@@ -145,19 +110,10 @@ 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);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
}
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : uris) {
@@ -165,18 +121,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
map = Arguments.createMap();
text = "file://" + filePath;
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
} else {
type = "application/octet-stream";
}
map.putString("type", type);
map.putString("type", RealPathUtil.getMimeType(filePath));
items.pushMap(map);
}
}

View File

@@ -5,22 +5,15 @@ import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.ReactPackage;
import com.mattermost.rnbeta.MainApplication;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SharePackage implements ReactPackage {
MainApplication mApplication;
public SharePackage(MainApplication application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ShareModule(mApplication, reactContext));
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png Executable file → Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 608 KiB

View File

@@ -1,3 +1,4 @@
<?xml version="1.0"?>
<resources>
<string name="app_name">Mattermost Beta</string>
<string name="inAppPinCode_title">in-App Pincode</string>

View File

@@ -3,6 +3,9 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:colorBackground">@color/white</item>
</style>
</resources>

View File

@@ -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>

View File

@@ -1,51 +1,25 @@
// 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 {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
}
}
}

View File

@@ -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

Binary file not shown.

View File

@@ -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
View File

@@ -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
View File

@@ -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

View File

@@ -1,10 +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'
project(':react-native-keychain').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keychain/android')
include ':react-native-doc-viewer'
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
include ':react-native-video'
@@ -15,8 +9,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'
@@ -39,9 +33,7 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/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')

View File

@@ -1,57 +1,55 @@
// 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 {networkStatusChangedAction} from 'redux-offline';
import {Client4} from 'mattermost-redux/client';
import {Dimensions} from 'react-native';
import {DeviceTypes} from 'app/constants';
export function calculateDeviceDimensions() {
const {height, width} = Dimensions.get('window');
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width
}
};
}
export function connection(isOnline) {
return async (dispatch) => {
Client4.setOnline(isOnline);
dispatch(networkStatusChangedAction(isOnline));
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
});
data: isOnline
}, getState);
};
}
export function setStatusBarHeight(height = 20) {
return {
type: DeviceTypes.STATUSBAR_HEIGHT_CHANGED,
data: height,
};
}
export function setDeviceDimensions(height, width) {
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width,
},
data: height
};
}
export function setDeviceOrientation(orientation) {
return {
type: DeviceTypes.DEVICE_ORIENTATION_CHANGED,
data: orientation,
data: orientation
};
}
export function setDeviceAsTablet() {
return {
type: DeviceTypes.DEVICE_TYPE_CHANGED,
data: true,
data: true
};
}
export default {
calculateDeviceDimensions,
connection,
setDeviceDimensions,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight,
setStatusBarHeight
};

View 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, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id,
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
}
};
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ViewTypes} from 'app/constants';
export function dismissBanner(text) {
return {
type: ViewTypes.ANNOUNCEMENT_BANNER,
data: text,
};
}

View File

@@ -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';
@@ -8,10 +8,9 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
getChannelByNameAndTeamName,
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';
@@ -19,21 +18,18 @@ 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 {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannel,
isGroupChannel,
isGroupChannel
} from 'mattermost-redux/utils/channel_utils';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox';
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
const MAX_POST_TRIES = 3;
@@ -44,18 +40,6 @@ export function loadChannelsIfNecessary(teamId) {
};
}
export function loadChannelsByTeamName(teamName) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
const team = getTeamByName(state, teamName);
if (team && team.id !== currentTeamId) {
await dispatch(fetchMyChannelsAndMembers(team.id));
}
};
}
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
return async (dispatch, getState) => {
const state = getState();
@@ -74,7 +58,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name,
value: 'true',
value: 'true'
};
}
@@ -145,7 +129,8 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
if (channel) {
actions.push({
type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
data: {id: channel.id, user_id: members[i]},
data: {user_id: members[i]},
id: channel.id
});
}
}
@@ -161,27 +146,13 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
const postsIds = postsInChannel[channelId];
const actions = [];
const time = Date.now();
let loadMorePostsVisible = true;
let received;
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
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,
},
});
}
} else {
const {lastConnectAt} = state.device.websocket;
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
@@ -198,36 +169,21 @@ 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,
},
});
}
}
if (received) {
actions.push({
dispatch({
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
channelId,
time,
time
});
}
actions.push(setLoadMorePostsVisible(loadMorePostsVisible));
dispatch(batchActions(actions));
};
}
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));
@@ -244,8 +200,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);
}
};
}
@@ -263,7 +219,7 @@ export function loadThreadIfNecessary(rootId, channelId) {
}
export function selectInitialChannel(teamId) {
return (dispatch, getState) => {
return async (dispatch, getState) => {
const state = getState();
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
@@ -278,54 +234,13 @@ 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));
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
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));
return;
}
dispatch(selectDefaultChannel(teamId));
};
}
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const {channels} = getState().entities.channels;
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
@@ -340,66 +255,31 @@ export function selectDefaultChannel(teamId) {
}
if (channelId) {
dispatch(handleSelectChannel(channelId));
dispatch(setChannelDisplayName(''));
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId)(dispatch, getState);
}
};
}
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);
dispatch(setLoadMorePostsVisible(true));
// If the app is open from push notification, we already fetched the posts.
if (!fromPushNotification) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
const {currentTeamId} = getState().entities.teams;
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
selectChannel(channelId),
setChannelDisplayName(channel.display_name),
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
data: channelId
},
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId,
},
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
member,
},
]));
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
}
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
export function handleSelectChannelByName(channelName, teamName) {
return async (dispatch, getState) => {
const state = getState();
const {teams: currentTeams, currentTeamId} = state.entities.teams;
const currentTeamName = currentTeams[currentTeamId].name;
const {data: channel} = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
const currentChannelId = getCurrentChannelId(state);
if (channel && currentChannelId !== channel.id) {
dispatch(handleSelectChannel(channel.id));
}
channelId
}
]), 'BATCH_CHANNEL_LOADED');
};
}
@@ -408,30 +288,63 @@ export function handlePostDraftChanged(channelId, draft) {
dispatch({
type: ViewTypes.POST_DRAFT_CHANGED,
channelId,
draft,
draft
}, getState);
};
}
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
const insertEvent = threadId ? INSERT_TO_COMMENT : INSERT_TO_DRAFT;
let draft;
let cursorPosition;
let action;
if (state.views.thread.drafts[threadId]) {
const threadDraft = state.views.thread.drafts[threadId];
draft = threadDraft.draft;
cursorPosition = threadDraft.cursorPosition;
action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId: threadId
};
} else if (state.views.channel.drafts[channelId]) {
const channelDraft = state.views.channel.drafts[channelId];
draft = channelDraft.draft;
cursorPosition = channelDraft.cursorPosition;
action = {
type: ViewTypes.POST_DRAFT_CHANGED,
channelId
};
}
EventEmitter.emit(insertEvent, value);
let nextDraft = `${value}`;
if (cursorPosition > 0) {
const beginning = draft.slice(0, cursorPosition);
const end = draft.slice(cursorPosition);
nextDraft = `${beginning}${value}${end}`;
}
if (action && nextDraft !== draft) {
dispatch({
...action,
draft: nextDraft
});
}
};
}
export function markChannelViewedAndRead(channelId, previousChannelId, markOnServer = true) {
return (dispatch) => {
dispatch(markChannelAsRead(channelId, previousChannelId, markOnServer));
dispatch(markChannelAsViewed(channelId, previousChannelId));
};
}
export function toggleDMChannel(otherUserId, visible, channelId) {
export function toggleDMChannel(otherUserId, visible) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
@@ -440,15 +353,10 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
user_id: currentUserId,
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
name: otherUserId,
value: visible,
}, {
user_id: currentUserId,
category: Preferences.CATEGORY_CHANNEL_OPEN_TIME,
name: channelId,
value: Date.now().toString(),
value: visible
}];
dispatch(savePreferences(currentUserId, dm));
savePreferences(currentUserId, dm)(dispatch, getState);
};
}
@@ -461,10 +369,10 @@ export function toggleGMChannel(channelId, visible) {
user_id: currentUserId,
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
name: channelId,
value: visible,
value: visible
}];
dispatch(savePreferences(currentUserId, gm));
savePreferences(currentUserId, gm)(dispatch, getState);
};
}
@@ -473,9 +381,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);
}
};
}
@@ -503,51 +411,39 @@ export function refreshChannelWithRetry(channelId) {
export function leaveChannel(channel, reset = false) {
return async (dispatch, getState) => {
const state = getState();
const {currentChannelId} = state.entities.channels;
const {currentTeamId} = state.entities.teams;
dispatch({
type: ViewTypes.REMOVE_LAST_CHANNEL_FOR_TEAM,
data: {
teamId: currentTeamId,
channelId: channel.id,
},
});
if (channel.id === currentChannelId || reset) {
await dispatch(selectDefaultChannel(currentTeamId));
const {currentTeamId} = getState().entities.teams;
await serviceLeaveChannel(channel.id)(dispatch, getState);
if (channel.isCurrent || reset) {
await selectInitialChannel(currentTeamId)(dispatch, getState);
}
await dispatch(serviceLeaveChannel(channel.id));
};
}
export function setChannelLoading(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_LOADER,
loading,
loading
};
}
export function setChannelRefreshing(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_REFRESHING,
loading,
loading
};
}
export function setChannelRetryFailed(failed = true) {
return {
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed,
failed
};
}
export function setChannelDisplayName(displayName) {
return {
type: ViewTypes.SET_CHANNEL_DISPLAY_NAME,
displayName,
displayName
};
}
@@ -559,83 +455,57 @@ export function increasePostVisibility(channelId, focusedPostId) {
const currentPostVisibility = postVisibility[channelId] || 0;
if (loadingPosts[channelId]) {
return true;
return;
}
// 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) {
// We already have the posts, so we just need to show them
dispatch(batchActions([
doIncreasePostVisibility(channelId),
setLoadMorePostsVisible(true),
]));
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
return true;
return;
}
}
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
channelId,
channelId
});
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
const page = Math.floor(currentPostVisibility / pageSize);
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let result;
if (focusedPostId) {
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
result = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
} else {
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const actions = [{
type: ViewTypes.LOADING_POSTS,
data: false,
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));
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
}
dispatch(batchActions(actions));
return hasMorePost;
};
}
function doIncreasePostVisibility(channelId) {
return {
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
};
}
function setLoadMorePostsVisible(visible) {
return {
type: ViewTypes.SET_LOAD_MORE_POSTS_VISIBLE,
data: visible,
dispatch({
type: ViewTypes.LOADING_POSTS,
data: false,
channelId
});
};
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {ViewTypes} from 'app/constants';
export function setLastUpgradeCheck() {
return {
type: ViewTypes.SET_LAST_UPGRADE_CHECK,
type: ViewTypes.SET_LAST_UPGRADE_CHECK
};
}

View File

@@ -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';
@@ -15,7 +14,7 @@ export function executeCommand(message, channelId, rootId) {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId,
parent_id: rootId
};
let msg = message;
@@ -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);
};
}
}

View File

@@ -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';
@@ -18,13 +18,13 @@ export function handleCreateChannel(displayName, purpose, header, type) {
display_name: 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);
}
};
}

View File

@@ -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,
};

View File

@@ -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';
@@ -29,16 +29,6 @@ export function addReactionToLatestPost(emoji, rootId) {
export function addRecentEmoji(emoji) {
return {
type: ViewTypes.ADD_RECENT_EMOJI,
emoji,
};
}
export function incrementEmojiPickerPage() {
return async (dispatch) => {
dispatch({
type: ViewTypes.INCREMENT_EMOJI_PICKER_PAGE,
});
return {data: true};
emoji
};
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function addFileToFetchCache(url) {
return {
type: ViewTypes.ADD_FILE_TO_FETCH_CACHE,
url
};
}

View File

@@ -1,15 +1,20 @@
// 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';
import FormData from 'form-data';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
import {parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
import {ViewTypes} from 'app/constants';
import {buildFileUploadData, generateId} from 'app/utils/file';
import {ViewTypes} from 'app/constants';
export function initUploadFiles(files, rootId) {
return (dispatch, getState) => {
export function handleUploadFiles(files, rootId) {
return async (dispatch, getState) => {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const clientIds = [];
files.forEach((file) => {
@@ -20,36 +25,28 @@ export function initUploadFiles(files, rootId) {
clientId,
localPath: fileData.uri,
name: fileData.name,
type: fileData.type,
extension: fileData.extension,
type: fileData.mimeType,
extension: fileData.extension
});
formData.append('files', fileData);
formData.append('channel_id', channelId);
formData.append('client_ids', clientId);
});
let formBoundary;
if (Platform.os === 'ios') {
formBoundary = '--mobile.client.file.upload';
}
dispatch({
type: ViewTypes.SET_TEMP_UPLOAD_FILES_FOR_POST_DRAFT,
clientIds,
channelId,
rootId,
rootId
});
};
}
export function uploadFailed(clientIds, channelId, rootId, error) {
return {
type: FileTypes.UPLOAD_FILES_FAILURE,
clientIds,
channelId,
rootId,
error,
};
}
export function uploadComplete(data, channelId, rootId) {
return {
type: FileTypes.RECEIVED_UPLOAD_FILES,
data,
channelId,
rootId,
await uploadFile(channelId, rootId, parseClientIdsFromFormData(formData), formData, formBoundary)(dispatch, getState);
};
}
@@ -58,13 +55,31 @@ export function retryFileUpload(file, rootId) {
const state = getState();
const channelId = state.entities.channels.currentChannelId;
const formData = new FormData();
const fileData = {
uri: file.localPath,
name: file.name,
type: file.type
};
formData.append('files', fileData);
formData.append('channel_id', channelId);
formData.append('client_ids', file.clientId);
let formBoundary;
if (Platform.os === 'ios') {
formBoundary = '--mobile.client.file.upload';
}
dispatch({
type: ViewTypes.RETRY_UPLOAD_FILE_FOR_POST,
clientId: file.clientId,
channelId,
rootId,
rootId
});
await uploadFile(channelId, rootId, [file.clientId], formData, formBoundary)(dispatch, getState);
};
}
@@ -72,7 +87,7 @@ export function handleClearFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FILES_FOR_POST_DRAFT,
channelId,
rootId,
rootId
};
}
@@ -80,7 +95,7 @@ export function handleClearFailedFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FAILED_FILES_FOR_POST_DRAFT,
channelId,
rootId,
rootId
};
}
@@ -89,7 +104,7 @@ export function handleRemoveFile(clientId, channelId, rootId) {
type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,
clientId,
channelId,
rootId,
rootId
};
}
@@ -97,6 +112,6 @@ export function handleRemoveLastFile(channelId, rootId) {
return {
type: ViewTypes.REMOVE_LAST_FILE_FROM_POST_DRAFT,
channelId,
rootId,
rootId
};
}

View File

@@ -1,25 +1,17 @@
// 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';
import {setCSRFFromCookie} from 'app/utils/security';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
loginId
}, getState);
};
}
@@ -28,40 +20,28 @@ export function handlePasswordChanged(password) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.PASSWORD_CHANGED,
password,
password
}, getState);
};
}
export function handleSuccessfulLogin() {
return async (dispatch, getState) => {
const state = getState();
const config = getConfig(state);
const license = getLicense(state);
const {config, license} = getState().entities.general;
const token = Client4.getToken();
const url = Client4.getUrl();
const deviceToken = state.entities.general.deviceToken;
const currentUserId = getCurrentUserId(state);
await setCSRFFromCookie(url);
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
}
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url,
token,
},
});
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: {}});
}
@@ -70,46 +50,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;
};
}
@@ -117,5 +73,5 @@ export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
scheduleExpiredNotification,
getSession
};

View File

@@ -1,65 +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 {ViewTypes} from 'app/constants';
import {
handleLoginIdChanged,
handlePasswordChanged,
} from 'app/actions/views/login';
jest.mock('app/mattermost', () => ({
app: {
setAppCredentials: () => jest.fn(),
},
}));
jest.mock('react-native-cookies', () => ({
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
openURL: jest.fn(),
canOpenURL: jest.fn(),
getInitialURL: jest.fn(),
get: () => Promise.resolve(({
res: {
MMCSRF: {
value: 'the cookie',
},
},
})),
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Login', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleLoginIdChanged', () => {
const loginId = 'email@example.com';
const action = {
type: ViewTypes.LOGIN_ID_CHANGED,
loginId,
};
store.dispatch(handleLoginIdChanged(loginId));
expect(store.getActions()).toEqual([action]);
});
test('handlePasswordChanged', () => {
const password = 'password';
const action = {
type: ViewTypes.PASSWORD_CHANGED,
password,
};
store.dispatch(handlePasswordChanged(password));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -1,34 +1,34 @@
// 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;
const channelName = getDirectChannelName(currentUserId, otherUserId);
const {channels, myMembers} = state.entities.channels;
dispatch(getProfilesByIds([otherUserId]));
dispatch(getStatusesByIds([otherUserId]));
getProfilesByIds([otherUserId])(dispatch, getState);
getStatusesByIds([otherUserId])(dispatch, getState);
let result;
let channel = Object.values(channels).find((c) => c.name === channelName);
if (channel && myMembers[channel.id]) {
result = {data: channel};
dispatch(toggleDMChannel(otherUserId, 'true', channel.id));
toggleDMChannel(otherUserId, 'true')(dispatch, getState);
} else {
result = await dispatch(createDirectChannel(currentUserId, otherUserId));
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
if (channel && switchToChannel) {
dispatch(handleSelectChannel(channel.id));
if (channel) {
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;
@@ -40,15 +40,15 @@ export function makeGroupChannel(otherUserIds) {
const state = getState();
const {currentUserId} = state.entities.users;
dispatch(getProfilesByIds(otherUserIds));
dispatch(getStatusesByIds(otherUserIds));
getProfilesByIds(otherUserIds)(dispatch, getState);
getStatusesByIds(otherUserIds)(dispatch, getState);
const result = await createGroupChannel([currentUserId, ...otherUserIds])(dispatch, getState);
const channel = result.data;
if (channel) {
dispatch(toggleGMChannel(channel.id, 'true'));
dispatch(handleSelectChannel(channel.id));
toggleGMChannel(channel.id, 'true')(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;

View File

@@ -1,70 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// 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';
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
return async (dispatch) => {
const timestamp = Date.now();
const post = {
id: generateId(),
user_id: user.id,
channel_id: channelId,
message,
type: Posts.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL,
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
parent_id: postRootId,
props: {
username: user.username,
addedUsername,
},
};
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[post.id]: post,
},
},
channelId,
});
};
}
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));
};
}

View File

@@ -1,33 +1,28 @@
// 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 {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';
export function startDataCleanup() {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.DATA_CLEANUP,
payload: getState(),
});
};
}
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction
} from 'app/actions/views/channel';
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const [configData, licenseData] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
const config = configData.data || {};
@@ -46,44 +41,41 @@ 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} = 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;
}
// load any missing data
const loading = [];
// when the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
//verify that we have the team loaded
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
loading.push(dispatch(getMyTeams()));
loading.push(dispatch(getMyTeamMembers()));
}
if (channelId && !channels[channelId]) {
loading.push(dispatch(fetchMyChannelsAndMembers(teamId)));
}
if (loading.length > 0) {
await Promise.all(loading);
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState)
]);
}
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
dispatch(selectTeam({id: teamId}));
selectTeam({id: teamId})(dispatch, getState);
}
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
// when the notification is from the same channel as the current channel
// we should get the posts
if (channelId === currentChannelId) {
markChannelAsRead(channelId, null, false)(dispatch, getState);
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
markChannelAsRead(channelId, currentChannelId, false)(dispatch, getState);
dispatch(setChannelDisplayName(''));
handleSelectChannel(channelId)(dispatch, getState);
}
};
}
@@ -91,10 +83,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;
@@ -105,26 +95,21 @@ export function createPostForNotificationReply(post) {
...post,
pending_post_id: pendingPostId,
create_at: timestamp,
update_at: timestamp,
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,15 +121,8 @@ export function recordLoadTime(screenName, category) {
};
}
export function setDeepLinkURL(url) {
return {
type: ViewTypes.SET_DEEP_LINK_URL,
url,
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,
purgeOfflineStore,
purgeOfflineStore
};

View File

@@ -1,23 +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 {setDeepLinkURL} from './root';
const mockStore = configureStore([thunk]);
describe('Actions.Views.Root', () => {
const store = mockStore();
test('should set deep link URL', async () => {
const url = 'https://test-url.com/team-name/pl/pl-id';
const action = {
type: 'SET_DEEP_LINK_URL',
url,
};
await store.dispatch(setDeepLinkURL(url));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -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';
@@ -9,29 +7,7 @@ export function handleSearchDraftChanged(text) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.SEARCH_DRAFT_CHANGED,
text,
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);
};
}

View File

@@ -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';
@@ -11,11 +11,11 @@ export function handleServerUrlChanged(serverUrl) {
dispatch(batchActions([
{type: GeneralTypes.CLIENT_CONFIG_RESET},
{type: GeneralTypes.CLIENT_LICENSE_RESET},
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl},
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl}
]), getState);
};
}
export default {
handleServerUrlChanged,
handleServerUrlChanged
};

View File

@@ -1,34 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {batchActions} from 'redux-batched-actions';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
const mockStore = configureStore([thunk]);
describe('Actions.Views.SelectServer', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleServerUrlChanged', async () => {
const serverUrl = 'https://mattermost.example.com';
const actions = batchActions([
{type: GeneralTypes.CLIENT_CONFIG_RESET},
{type: GeneralTypes.CLIENT_LICENSE_RESET},
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl},
]);
store.dispatch(handleServerUrlChanged(serverUrl));
expect(store.getActions()).toEqual([actions]);
});
});

View File

@@ -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,47 +20,40 @@ 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);
};
}
export function selectDefaultTeam() {
export function selectFirstAvailableTeam() {
return async (dispatch, getState) => {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const {teams: allTeams, myMembers} = state.entities.teams;
const {teams: allTeams, myMembers} = getState().entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
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);
if (firstTeam) {
handleTeamChange(firstTeam.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);
}
};
}
export default {
handleTeamChange,
selectDefaultTeam,
selectFirstAvailableTeam
};

View File

@@ -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';
@@ -8,7 +8,7 @@ export function handleCommentDraftChanged(rootId, draft) {
dispatch({
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId,
draft,
draft
}, getState);
};
}
@@ -17,6 +17,6 @@ export function handleCommentDraftSelectionChanged(rootId, cursorPosition) {
return {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition,
cursorPosition
};
}

View File

@@ -1,46 +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 {ViewTypes} from 'app/constants';
import {
handleCommentDraftChanged,
handleCommentDraftSelectionChanged,
} from 'app/actions/views/thread';
const mockStore = configureStore([thunk]);
describe('Actions.Views.Thread', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test('handleCommentDraftChanged', async () => {
const rootId = '1234';
const draft = 'draft1';
const action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId,
draft,
};
await store.dispatch(handleCommentDraftChanged(rootId, draft));
expect(store.getActions()).toEqual([action]);
});
test('handleCommentDraftSelectionChanged', async () => {
const rootId = '1234';
const cursorPosition = 'position';
const action = {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition,
};
await store.dispatch(handleCommentDraftSelectionChanged(rootId, cursorPosition));
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -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';

View File

@@ -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,
},
});
};
}

Some files were not shown because too many files have changed in this diff Show More