Compare commits

..

7 Commits

Author SHA1 Message Date
enahum
fc179a6516 translations PR 20170905 (#882) 2017-09-05 09:46:41 -03:00
lfbrock
fea6372819 Update minimum server version (#878) 2017-09-02 21:29:47 -03:00
enahum
87e89de854 Version Bump to 49 (#876) 2017-09-01 14:50:01 -03:00
enahum
534af426c9 Version Bump to 49 (#875) 2017-09-01 14:49:48 -03:00
enahum
553f3796b1 RN-220 Add "in:" and "from:" search modifiers (#869)
* Fix search and search preview

* Add "in:" and "from:" search modifiers
2017-08-24 18:07:07 -03:00
enahum
18b3d6eec9 Fix badge display on Android (#867) 2017-08-24 11:31:03 -03:00
Harrison Healey
73c81bb863 RN-73 Fixed code block text colour being incorrect (#868) 2017-08-24 10:59:03 -03:00
1231 changed files with 61505 additions and 117113 deletions

11
.babelrc Normal file
View File

@@ -0,0 +1,11 @@
{
"presets": [ "react-native" ],
"plugins": [
["module-resolver", {
"root": ["./src", "."],
"alias": {
"assets": "./dist/assets"
}
}]
]
}

View File

@@ -1,23 +0,0 @@
version: 2.1
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: circleci/node:10
steps:
- checkout
- run: |
echo assets/base/config.json
cat assets/base/config.json
# Avoid installing pods
touch .podinstall
# Run tests
make test || exit 1
workflows:
version: 2
pr-test:
jobs:
- test

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

@@ -1,70 +1,58 @@
[ignore]
; We fork some components by platform
# We fork some components by platform.
.*/*[.]android.js
; Ignore "BUCK" generated dirs
# Ignore templates with `@flow` in header
.*/local-cli/generator.*
# Ignore malformed json
.*/node_modules/y18n/test/.*\.json
# Ignore the website subdir
<PROJECT_ROOT>/website/.*
# Ignore BUCK generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
# Ignore unexpected extra @providesModule
.*/node_modules/commoner/test/source/widget/share.js
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
# Ignore duplicate module providers
# For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
.*/Libraries/polyfills/.*
; Ignore metro
.*/node_modules/metro/.*
.*/Libraries/react-native/ReactNative.js
.*/node_modules/jest-runtime/build/__tests__/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/
node_modules/react-native/flow
flow/
[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/.*
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
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\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.92.0
^0.32.0

43
.gitignore vendored
View File

@@ -1,10 +1,5 @@
assets/override
dist
build-ios
*.zip
server.PID
mattermost.keystore
tmp/
# OSX
#
@@ -22,7 +17,6 @@ build/
*.perspectivev3
!default.perspectivev3
xcuserdata
xcshareddata
*.xccheckout
*.moved-aside
DerivedData
@@ -31,28 +25,29 @@ DerivedData
*.apk
*.xcuserstate
project.xcworkspace
xcshareddata/
# Android/IntelliJ
# Android/IJ
#
*.iml
.idea
.gradle
local.properties
*.iml
# node.js
#
node_modules/
npm-debug.log
.npminstall
yarn-error.log
# yarn
#
.yarninstall
# BUCK
buck-out/
\.buckd/
android/app/libs
*.keystore
android/keystores/debug.keystore
# Vim
[._]*.s[a-w][a-z]
@@ -63,33 +58,13 @@ Session.vim
*~
tags
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# 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
*.zip
server.PID
mattermost.keystore
# Sentry
android/sentry.properties
ios/sentry.properties
# Testing
.nyc_output
coverage
# Pods
.podinstall
ios/Pods/
# Bundle artifact
*.jsbundle
#editor-settings
.vscode

View File

@@ -1,817 +1,9 @@
# Mattermost Mobile Apps Changelog
## 1.21.0 Release
- Release Date: July 16, 2019
- Server Versions Supported: Server v5.9+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed a few mobile app crash / fatal error issues.
- Fixed an issue where having the sidebar open at all times on tablets did not work for split view.
- Fixed an issue where new messages were often hidden behind a keyboard or text field.
- Fixed an issue on Android where channel sorting didn't match the web app.
- Fixed an issue where sharing a GIF via keyboard resulted in an error screen.
- Fixed an issue where long-press menu could not be dragged up when rotating the device to landscape view while the menu was open.
- Fixed an issue on Android where push notification settings were only saved after closing the settings page.
- Fixed an issue where users on View Members list had an icon that appeared to be selectable but was not.
- Fixed an issue where "Jump To" showed archived channels the user did not belong to instead of the ones the user was a member of.
- Fixed an issue where changing the timezone setting manually to "Set automatically" did not work on the mobile app.
- Fixed an issue where setting a position field for AD/LDAP sync or SAML in the System Console did not block the user from changing it in account settings.
- Fixed an issue where **Channel Info > Manage/View Members** screen didn't load channel users.
- Fixed an issue where enabling large fonts on iOS caused the left-hand side text to be cut off.
- Fixed an issue on Android where users could not reply to a push notification if the mention was in a thread message.
### Known Issues
- (Android) On subpath server, logging in using GitLab or OneLogin fails to display Mattermost. [MM-16829](https://mattermost.atlassian.net/browse/MM-16829)
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
- Android apps slow down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
## 1.20.2 Release
- Release Date: July 10, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where Moto G7 devices were detected as tablets and showed a fixed width sidebar.
- Fixed an issue where having the sidebar open at all times on tablets did not work on split view.
## 1.20.1 Release
- Release Date: June 21, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where some Android devices were crashing.
- Fixed an issue where messages were missing after reconnecting the network.
## 1.20.0 Release
- Release Date: June 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Highlights
#### Tablet Improvements
- Channel sidebar now remains open at a fixed width on tablet devices.
#### iOS Keyboard Dismissal
- If the keyboard is open, swiping down past it now closes it.
#### Profile Telemetry for Android Beta Builds
- To improve Android app performance, we are collecting trace events and device information, collectively known as metrics, to identify slow performing key areas. Those metrics will be sent only from users using Android app beta build starting in version v1.20, who are logged in to servers that allow sending [diagnostic information](https://docs.mattermost.com/administration/config-settings.html#enable-diagnostics-and-error-reporting).
### Improvements
- Increased the double tap delay for post action buttons.
- Implemented assets for Adaptive icons.
- Users are now brought to the bottom of the channel when posting a message.
- Users can now execute actions while the keyboard is open.
- Added support on iOS for IPv6 on LTE networks.
- Added support for LDAP Group constrained feature with v5.12 servers.
### Bug Fixes
- Fixed an issue where a post wasn't immediately removed when deleting another user's post.
- Fixed an issue where the cursor jumped back when typing after auto-completing a slash command.
- Fixed an issue where the iOS app didnt properly restore its connection after disconnect.
- Fixed an issue where the long press menu persisted after returning from a thread.
- Fixed an issue on Android where the "Write to [channel name]" was cut off for group messages with several users.
- Fixed an issue where users were not able to flag or unflag posts in a read-only channel.
- Fixed an issue where the progress indicator was negative while downloading a video.
- Fixed an issue where the edit post modal didnt have an autocorrect.
- Fixed an issue where the 'I forgot my password' option was available on the mobile client even with Email Authentication disabled on the server.
- Fixed an issue with large separation between placeholders on iPad when a channel was loading.
- Fixed an issue where "Show More" was not removed after the post was edited to a single line.
### Known Issues
- Buttons inside ephemeral posts are not clickable / functional on the mobile app. [MM-15084](https://mattermost.atlassian.net/browse/MM-15084)
- App slows down when opening a channel with large number of animated emoji. [MM-15792](https://mattermost.atlassian.net/browse/MM-15792)
## 1.19.0 Release
- Release Date: May 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed an issue where Android managed config was lost on the thread view.
- Fixed an issue where contents of ephemeral posts did not display on the mobile app.
- Fixed a few mobile app crash / fatal error issues.
- Fixed an issue with an expanding animation when tapping on Jump to Channel in the channel list.
- Fixed an issue on iOS where animated custom emoji weren't animated.
- Fixed an issue on iOS where users were unable to create channel name of 2 characters.
- Fixed an issue on iOS where emoji appeared too close, with uneven spacing, and too small in the info modal.
- Added an error handler when sharing text that was over server's maximum post size with the iOS Share Extension.
- Fixed an issue where users could upload a GIF as a profile image.
### Known Issues
- Buttons inside ephemeral posts are not clickable / functional on the mobile app.
## 1.18.1 Release
- Release Date: April 18, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Bug Fixes
- Fixed a crash issue caused by a malformed post textbox localize string.
- Fixed an issue where iOS crashed when trying to log in using SSO and the SSO provider set a cookie without an expiration date.
## 1.18.0 Release
- Release Date: April 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
- ``Bot`` tags were added for bot accounts feature in server v5.10 and mobile v1.18, meaning that mobile v1.17 and earlier don't support the tags.
### Highlights
- Added support for Office365 single sign-on (SSO).
- Added support for Integrated Windows Authentication (IWA).
### Improvements
- Added the ability for channel links to open inside the app.
- Added ability for emojis and hyperlinks to render in the message attachment title.
- Added Chinese support for words that trigger mentions.
- Added a setting to the system console to change the minimum length of hashtags.
- Added a reply option to long press context menu.
### Bug Fixes
- Fixed an issue where blank spaces broke markdown tables.
- Fixed an issue where deactivated users appeared on "Add Members" modal but not on the search results.
- Fixed an issue on Android where extra text in the search box appeared after using the autocomplete drop-down.
- Fixed an issue with multiple text entries when typing with Shift+Letter on Android.
- Fixed an issue where push notifications badges did not always clear when read on another device.
- Fixed an issue where opening a single or group notification did not take the user into the channel where the notification came from.
- Fixed an issue where timezone did not automatically update on Android when travelling to another timezone.
- Fixed an issue where the user mention autocomplete drop-down was case sensitive.
- Fixed an issue where system admininistrators were able to see the full long press menu when long pressing a system message.
- Fixed an issue where users were not able to unflag posts from "Flagged Posts" when opened from a read-only channel.
- Fixed an issue where users were unable to create channel names of 2 byte characters.
### Known Issues
- Content for ephemeral messages is not displayed on Mattermost Mobile Apps.
## 1.17.0 Release
- Release Date: March 20, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- If **DisableLegacyMfa** setting in ``config.json`` is set to ``true`` and [multi-factor authentication](https://docs.mattermost.com/deployment/auth.html) is enabled, ensure your users have upgraded to mobile app version 1.17 or later. See [Important Upgrade Notes](https://docs.mattermost.com/administration/important-upgrade-notes.html) for more details.
- If you are using an EMM provider via AppConfig, make sure to add two new settings, `useVPN` and `timeoutVPN`, to your AppConfig file. The settings were added for EMM connections using VPN on-demand - one to indicate if every request should wait for the VPN connection to be established, and another to set the timeout in seconds. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
- iPhone 5s devices and later with iOS 11+ is required.
### Highlights
- iOS Share Extension now supports large file sizes and improved performance
### Bug Fixes
- Fixed support for EMM connections using VPN on-demand. See docs for more details on [setting AppConfig values](https://docs.mattermost.com/mobile/mobile-appconfig.html#mattermost-appconfig-values) for VPN support.
- Fixed several Android app crash / fatal error issues.
- Fixed an issue on Android where the app crashed intermittently when selecting a link.
- Fixed an issue where email notifications setting was out of sync with the webapp until the setting was edited.
- Fixed an issue where notification badges were not cleared from other clients when clicking on a push notification after opening the mobile app.
- Fixed an issue where the app did not show local notification when session expired.
- Fixed an issue where the profile picture for webhooks was showing the hook owner picture.
- Fixed an issue where some emoji were not rendered as jumbo.
- Fixed an issue where jumbo emoji posted as a reply sometimes appeared with large space beneath.
- Fixed an issue where the "No Internet Connection" banner did not always display when internet connectivity was lost.
- Fixed an issue where the "No Internet Connection" banner did not always disappear when connection was re-estabilished.
- Fixed an issue where opening channels with unreads had loading indicator placed above unread messages line.
## 1.16.1 Release
- Release Date: February 21, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue where link previews and reactions weren't displayed when post metadata was disabled.
- Fixed an issue on Android where the app crashed when sharing multiple files.
## 1.16.0 Release
- Release Date: February 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Improvements
- Added the ability to remove own profile picture.
- Changed "X" to "Cancel" on Edit Profile page.
- Added support for relative permalinks.
### Bug Fixes
- Fixed an issue where the iOS app did not wait until the on-demand VPN connection was established. (EMM Providers)
- Fixed an issue with a white screen caused by missing Russian translations.
- Fixed an issue where the iOS badge notification did not always clear.
- Fixed an issue where the thread view displayed a new message indicator.
- Fixed an issue where quick multiple taps on the file icon opened multiple file previews.
- Fixed an issue where the settings page did not show an option to join other teams.
- Fixed an issue where image previews didn't work after using Delete File Cache.
- Fixed an issue on Android where the notification trigger word modal title was "Send email notifications" instead of "Keywords".
- Fixed an issue where the Webhook icon was misaligned and bottom edges were cut off.
- Fixed an issue on Android where the user was not asked to authenticate to the app first when trying to share a photo, resulting in a white "Share modal" screen with a never-ending loading indicator.
- Fixed an issue on iOS where push notifications were not preserved when opening the app via the Mattermost icon.
## 1.15.2 Release
- Release Date: January 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue where the status changes for other users did not always stay current in the mobile app.
- Fixed an issue where a post did not fail properly when the user attempted to send the post while there was no network access.
- Fixed an issue where date separators did not update when changing timezones.
- Fixed an issue where the Favorites section did not clear from a users's channel drawer.
- Removed an extra divider below "Edit Channel" of Direct Message Channel Info.
- Fixed an issue where a user was not returned to previously viewed channel after viewing and then closing an archived channel.
- Fixed an issue where a quick double tap on switch of Channel Info created and extra on/off state.
- Fixed an issue where iOS long press menu didn't have rounded corners.
## 1.15.1 Release
- Release Date: December 28, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue preventing some users from logging in using OKTA.
## 1.15.0 Release
- Release Date: December 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Highlights
- Added mention and reply mention highlighting.
- Added a sliding animation for the reaction list.
- Added support for pinned posts.
- Added support for jumbo emojis.
- Added support for interactive dialogs.
- Improved UI for the long press menu and emoji reaction viewer.
### Improvements
- Added the ability to include custom headers with requests for custom builds.
- Push Notifications that are grouped by channels are cleared once the channel is read.
- Improved auto-reconnect when unable to reach the server.
- Added support for changing the mobile client status to offline when the app loses connection.
- Added 'View Members' button to archived channels.
- Added support on iOS for keeping the postlist in place without scrolling when new content is available.
### Bug Fixes
- Fixed an issue where clicking on a file did not show downloading progress.
- Fixed an issue on Android where on fresh install the share extension would not properly show available channels.
- Fixed an issue where recently archived channels remained in in: autocomplete when they had been archived.
- Fixed an issue where text should render when no actual custom emoji matched the named emoji pattern.
- Fixed an issue on iOS where text got cut-off after replying to a message.
- Fixed an issue where search modifier for channels was showing Direct Messages without usernames.
- Fixed an issue where "Close Channel" did not work properly when viewing two archived channels in a row.
- Fixed an issue with "Critical Error" screen when trying to upload certain file types from "+" to the left of message input box.
## 1.14.0 Release
- Release Date: November 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue where the Android app did not allow establishing a network connection with any server that used a self-signed certificate that had the CA certificate user installed on the device.
- Removed "Copy Post" option on long-press message menu for posts without text.
- Fixed an issue where the "Search Results" header was not fully scrolled to top on search "from:username".
- Fixed an issue where channel names truncated at fewer characters than necessary.
- Fixed an issue where the same uploaded photo generated a different file size.
- Fixed an issue where the "(you)" was not displayed to the right of a user's name in the channel drawer when a user opened a Direct Message channel with themself.
- Fixed an issue where a dark theme set from webapp broke mobile display.
- Fixed an issue where channel drawer transition sometimes lagged.
- Fixed an issue where sending photos to Mattermost created large files.
- Fixed an issue where the apps showed "Select a Team" screen when opened.
- Fixed an issue where at-mention, emoji, and slash command autocompletes had a double top border.
- Fixed an issue where the drawer was unable to close when showing the team list.
- Fixed an issue where team sidebar showed + sign even without more teams to join.
## 1.13.1 Release
- Release Date: October 18, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue preventing some users from authenticating using OKTA
## v1.13.0 Release
- Release Date: October 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Highlights
#### View Emoji Reactions
- Hold down on any emoji reaction to see who reacted to the post.
#### Hashtags
- Added support for searching for hashtags in posts.
#### Dropdown menus
- Added support for dropdown menus in message attachments.
### Improvements
- Added support for iPhone XR, XS and XS Max.
- Added support for nicknames on user profile.
- On servers 5.4+, added support for searching in direct and group message channels using the "in:" modifier.
- Channel autocomplete now gets closed if multiple tildes are typed.
- Added a draft icon in sidebar and channel switcher for channels with unsent messages.
- Users are now redirected to the archived channel view (rather than to Town Square) when a channel is archived.
- When closing an archived channel, users are now returned to the previously viewed channel.
### Bug Fixes
- Refactored postlist to include Android Pie fixes and smoother scrolling.
- Fixed an issue where deactivated users were not marked as such in "Jump To" search.
- Fixed an issue where users got a permission error when trying to open a file from within the image preview screen.
- Fixed an issue where session expiry notifications were not being sent on Android.
- Fixed an issue where post attachments failed to upload.
- Fixed an issue where the "DM More..." list cut off user info.
- Fixed an issue where the user would briefly see a system message when loading a reply thread.
- Fixed an issue where the error message was incorrectly formatted if the login method was set to email/password and the user tried to log in with SAML.
- Fixed an issue on Android where the keyboard sometimes overlapped the bottom of the post textbox.
- Fixed an issue where there was no option to take video via "+" > "Take Photo or Video" on iOS.
## v1.12.0 Release
- Release Date: September 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Search Date Filters
- Search for messages before, on, or after a specified date.
### Improvements
- Added notification support for Android O and P.
### Bug Fixes
- Fixed an issue where Okta was not able to login in some deployments.
- Fixed an issue where messages in Direct Message channels did not show when clicking "Jump To".
- Fixed an issue where `Show More` on a post with a message attachment displayed a blank where content should have been.
- Prevent downloading of files when disallowed in the System Console.
- Fixed an issue where users could not click on attachment filenames to open them.
- Fixed an issue where email notification settings did not save from mobile.
- Fixed an issue where the share extension allowed users to select and attempt to share content to channels that had been archived.
- Fixed an issue where reacting to an existing emoji in an archived channel was allowed.
- Fixed an issue where archived channels sometimes remained in the drawer.
- Fixed an issue where deactivated users were not marked as such in Direct Message search.
## v1.11.0 Release
- Release Date: August 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Searching Archived Channels
- Added ability to search for archived channels. Requires Mattermost server v5.2 or later.
#### Deep Linking
- Added the ability for custom builds to open Mattermost links directly in the app rather than the default mobile browser. Learn more in our [documentation](https://docs.mattermost.com/mobile/mobile-faq.html#how-do-i-configure-deep-linking)
### Improvements
- Added profile pop-up to combined system messages.
- Force re-entering SSO auth credentials after logout.
- Added consecutive posts by the same user.
- Added a loading indicator when user info is still loading in the left-hand side.
### Bug Fixes
- Fixed an issue where Android devices showed an incorrect timestamp.
- Fixed an issue on Android where the app did not get sent to the background when pressing the hardware back button in the channel screen.
- Fixed an issue with video playback when the filename had spaces.
- Fixed an issue where the app crashed when playing YouTube videos.
- Fixed an issue with session expiration notification.
- Fixed an issue with sharing files from Google Drive in Android Share Extension.
- Fixed an issue on Android where replying to a push notification sometimes went to the wrong channel.
- Fixed an issue where the previous server URL was present on the input textbox before changing the screen to Login.
- Fixed an issue where user menu was not translated correctly.
- Fixed an issue where some field lengths in Account Settings didn't match the desktop app.
- Fixed an issue where long URLs for embedded images in message attachments got cut off and didn't render.
- Fixed an issue where link preview images were not cropped properly.
- Fixed an issue where long usernames didn't wrap properly in the Account Settings menu.
- Fixed an issue where DMs would not open if users were using "Jump To".
- Fixed an issue where no message was displayed after removing a user from a channel with join/leave messages disabled.
## v1.10.0 Release
- Release Date: July 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Channel drawer performance
- Android devices will notice significant performance improvements when opening and closing the channel drawer.
#### Channel loading performance
- Improved channel loading performance as post are retrieved with every push notification
#### Announcement banner improvements
- Markdown now renders when announcement banners are expanded
- When enabled by the System Admin, users can now dismiss announcement banners until their next session
### Improvements
- Combined consecutive messages from the same user.
- Added experimental support for certificate-based authentication (CBA) for iOS to identify a user or a device before granting access to Mattermost. See [documentation](https://docs.mattermost.com/deployment/certificate-based-authentication.html) to learn more.
- Added support for the experimental automatic direct message replies feature.
- Added support for the experimental timezone feature.
- Changed post textbox to not be a connected component.
- Allow connecting to mattermost instances hosted at subpaths.
- Added support for starting YouTube videos at a given time.
- Added support for keeping messages if slash command fails.
### Bug Fixes
- Fixed an issue where the unread badge background was always white.
- Fixed an issue where a username repeated in system message if user was added to a channel more than once.
- Fixed an issue where Android Sharing from Microsoft apps failed.
- Fixed an issue where YouTube crashed the app if link did not have a time set.
- Fixed an issue where System Admins did not see all teams available to join on mobile.
- Fixed an issue where users were unable to share from Files app.
- Fixed an issue where viewing a non-existent permalink didn't show an error message.
- Fixed an issue where jumping to a channel search did not bold unread channels.
- Fixed an issue with being able to add own user to a Group Message channel.
- Fixed an issue with not being able to reply from a push notification on iOS.
- Fixed an issue where the app did not display Brazilian language.
## 1.9.3 Release
- Release Date: July 04, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed multiple issues causing app crashes
- Fixed an issue on iOS devices with typing non-english characters in the post input box
## 1.9.2 Release
- Release Date: June 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where attached videos did not play for the poster
- Fixed an issue where "Jump to recent messages" from the permalink view did not direct the user to the bottom of the channel
- Fixed an issue where post comments did not identify which parent post they belonged to
- Fixed multiple issues with typing non-english characters in the post input box
- Fixed multiple issues causing random app crashes
- Fixed an issue where files from the Android Files app failed to upload
- Fixed an issue where the iOS share extension crashed when switching the team or channel
- Fixed an issue where files from the Microsoft app failed to upload
- Fixed an issue on Android devices where sharing files changed the file extension of the attachment
## 1.9.1 Release
- Release Date: June 23, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with typing lag on Android devices
- Fixed an issue causing users to be logged out after upgrading to v1.9.0
- Fixed an issue where the ``in:`` and ``from:`` modifiers were not being added to the search field
## v1.9.0 Release
- Release Date: June 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Improved first load time on Android
- Significantly decreased first load time on Android devices from cold start.
#### iOS Files app support
- Added support for attaching files from the iOS Files app from within Mattermost.
#### Improved styling of push notification
- Improved the layout of message content, channel name and sender name in push notifications.
### Improvements
- Combined join/leave system messages.
- Added splash screen and channel loader improvements.
- Removed the desktop notification duration setting.
- Added cache team icon and set background to always be white if using a PNG file.
- Added whitelabel for icons and splash screen.
### Bug Fixes
- Fixed an issue where other user's display name did not render in combined system messages after joining the channel.
- Fixed an issue where posts incorrectly had "Commented on Someone's message" above them.
- Fixed an issue where deleting a post or its parent in permalink view left permalink view blank.
- Fixed an issue where "User is typing" message cut was off.
- Fixed an issue where `More New Messages Above` appeared at the top of new channel on joining.
- Fixed an issue where a user was not directed to Town Square when leaving a channel.
- Fixed an issue where long post were not collapsed on Android.
- Fixed an issue where a user's name was initially shown as "someone" when opening a direct message with the user.
- Fixed an issue where an error was received when trying to change the team or channel from the share extension.
- Fixed an issue where switching to a newly created channel from a push notification redirected a user to Town Square.
- Fixed an issue where a public channel made private did not disappear automatically from clients not part of the channel.
## v1.8.0 Release
- Release Date: April 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Image performance
- Images are now downloaded and stored locally for better performance
#### Flagged Posts and Recent Mentions
- Access all your flagged posts and recent mentions from the buttons in the sidebar
#### Muted Channels
- Added support for Muted Channels released with Mattermost server v4.9
### Improvements
- Date separators now appear between each posts in the search view
- Deactivated users are now filtered out of the channel members lists
- Direct Messages user list is now sorted by username first
- Added the option to Direct Message yourself from your user profile screen
- Improved performance on the post list
- Improved matching and display when searching for users in the Direct Message user list
### Bug Fixes
- Fixed an issue where emoji reactions could be added from the search view but did not appear
- Fixed an issue causing the app to crash when trying to share content from a custom keyboard
- Fixed an issue where team names were being sorted based on letter case
- Fixed an issue where username would not be inserted to the post draft when using experimental configuration settings
- Fixed an issue with nested bullet lists being cut off in the user interface
- Fixed an issue where private channels were listed in the public channels section of the channel autocomplete list
- Fixed an issue where a profile images could not be updated from the app
## v1.7.1 Release
- Release Date: April 3, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where the iOS share extension sometimes crashed the Mattermost app
- Fixed an issue preventing Markdown tables from rendering with some international characters
## v1.7.0 Release
- Release Date: March 26, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### iOS File Sharing
- Share files and images from other applications as attached files in Mattermost
#### Markdown Tables
- Tables created using markdown formatting can now be viewed in the app
#### Permalinks
- Permalinks now open in the app instead of launching a browser window
### Improvements
- Increased the tappable area of various icons for improved usability
- Announcement banners now display in the app
- Added "+" button to add emoji reactions to a post
- Minor performance improvements for app launch time
- Text files can now be viewed in the app
- Support for email autolinking into the app
### Bugs
- Fixed an issue causing some devices to hang at the splash screen on app launch
- Fixed an issue causing some letters to be hidden in the Android search input box
- Fixed an issue causing some Direct Message channels to show date stamps below the most recent message
- Fixed an issue where users weren't able to join open teams they've never been a member of
- Fixed an issue so double tapping buttons can no longer cause UI issues
- Fixed an issue where changing the channel display name wasn't being updated in the UI appropriately
- Fixed an issue where searhing for public channels sometimes showed no results
- Fixed an issue where the post menu could remain open while scrolling in the post list
- Fixed an issue where the system message to add users to a channel was missing the execution link
- Fixed an issue where bulleted lists cut off text if nested deeper than two levels
- Fixed an issue where logging into an account that is not on any team freezes the app
- Fixed an issue on iOS causing the app to crash when taking a photo then attaching it to a post
## v1.6.1 Release
- Release Date: February 13, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue preventing the app from going to the correct channel when opened from a push notification
- Fixed an issue on Android devices where the app could sometimes freeze on the launch screen
- Fixed an issue on Samsung devices causing extra letters to be insterted when typing to filter user lists
## v1.6.0 Release
- Release Date: February 6, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Android File Sharing
- Share files and images from other applications as attached files in Mattermost
### Improvements
- Added a right drawer to access settings, edit profile information, change online status and logout
- Added support for opening a Direct Message channel with yourself
### Bugs
- Fixed a number of issues causing crashes on Android devices
- Fixed an issue with auto capitalization on Android keyboards
- Fixed an issue where the GitLab SSO login button sometimes didn't appear
- Fixed an issue with link previews not appearing on some accounts
- Fixed an issue where logging out of the app didn't clear the notification badge on the homescreen icon
- Fixed an issue where interactive message buttons would not wrap to a new line
- Fixed an issue where the keyboard would sometimes overlap the text input box
- Fixed an issue where the Direct Message channel wouldn't open from the profile page
- Fixed an issue where posts would sometimes overlap
- Fixed an issue where the app sometimes hangs on logout
## v1.5.3 Release
- Release Date: February 1, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
- Fixed a login issue when connecting to servers running a Data Retention policy
## v1.5.2 Release
- Release Date: January 12, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue causing some Android devices to crash on launch
- Fixed an issue with the app occasionally crashing when receiving push notifications in a new channel
- Channel footer area is now refreshed when switching between Group and Direct Message channels
- Fixed an issue on some Android devices so Mattermost verifies it has permissions to access ringtones
- Fixed an issue where the text box overlapped the keyboard on some iOS devices using multiple keyboard layouts
- Fixed an issue with video uploads on Android devices
- Fixed an issue with GIF uploads on iOS devices
- Fixed an issue with the mention badge flickering on the channel drawer icon when there were over 10 unread mentions
- Fixed an issue with the app occasionally freezing when requesting the RefreshToken
## v1.5.1 Release
- Release Date: December 7, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with the upgrade app screen showing with a transparent background
- Fixed an issue with clearing or replying to notifications sometimes crashing the app on Android
- Fixed an issue with the app sometimes crashing due to a missing function in the swiping control
## v1.5 Release
- Release Date: December 6, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### File Viewer
- Preview videos, RTF, PDFs, Word, Excel, and Powerpoint files
#### iPhone X Compatibility
- Added support for iPhone X
#### Slash Commands
- Added support for using custom slash commands
- Added support for built-in slash commands /away, /online, /offline, /dnd, /header, /purpose, /kick, /me, /shrug
### Improvements
- In iOS, 3D touch can now be used to peek into a channel to view the contents, and quickly mark it as read
- Markdown images in posts now render
- Copy posts, URLs, and code blocks
- Opening a channel with Unread messages takes you to the "New Messages" indicator
- Support for data retention, interactive message buttons, and viewing Do Not Disturb statuses depending on the server version
- (Edited) indicator now shows up beside edited posts
- Added a "Recently Used" section for emoji reactions
### Bug Fixes
- Android notifications now follow the default system setting for vibration
- Fixed app crashing when opening notification settings on Android
- Fixed an issue where the "Proceed" button on sign in screen stopped working after pressing logout multiple times
- HEIC images posted from iPhones now get converted to JPEG before uploading
## v1.4.1 Release
Release Date: Nov 15, 2017
Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed network detection issue causing some people to be unable to access the app
- Fixed issue with lag when pressing send button
- Fixed app crash when opening notification settings
- Fixed various other bugs to reduce app crashes
## v1.4 Release
- Release Date: November 6, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Performance improvements
- Various performance improvements to decrease channel load times
### Bug Fixes
- Fixed issue with Android app sometimes showing a white screen when re-opening the app
- Fixed an issue with orientation lock not working on Android
## v1.3 Release
- Release Date: October 5, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Tablet Support (Beta)
- Added support for landscape view, so the app may be used on tablets
- Note: Tablet support is in beta, and further improvements are planned for a later date
#### Link Previews
- Added support for image, GIF, and youtube link previews
#### Notifications
- Android: Added the ability to set light, vibrate, and sound settings
- Android: Improved notification stacking so most recent notification shows first
- Updated the design for Notification settings to improve usability
- Added the ability to reply from a push notification without opening the app (requires Android v7.0+, iOS 10+)
- Increased speed when opening app from a push notification
#### Download Files
- Added the ability to download all files on Android and images on iOS
### Improvements
- Using `+` shortcut for emoji reactions is now supported
- Improved emoji formatting (alignment and rendering of non-square aspect ratios)
- Added support for error tracking with Sentry
- Only show the "Connecting..." bar after two connection attempts
### Bug Fixes
- Fixed link rendering not working in certain cases
- Fixed theme color issue with status bar on Android
## v1.2 Release
- Release Date: September 5, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### AppConfig Support for EMM solutions
- Added [AppConfig](https://www.appconfig.org/) support, to make it easier to integrate with a variety of EMM solutions
#### Code block viewer
- Tap on a code block to open a viewer for easier reading
### Improvements
- Updated formatting for markdown lists and code blocks
- Updated formatting for `in:` and `from:` search autocomplete
### Emoji Picker for Emoji Reactions
- Added an emoji picker for selecting a reaction
### Bug Fixes
- Fixed issue where if only LDAP and GitLab login were enabled, LDAP did not show up on the login page
- Fixed issue with 3 digit mention count UI in channel drawer
### Known Issues
- Using `+:emoji:` to react to a message is not yet supported
## v1.1 Release
- Release Date: August 2017
- Server Versions Supported: Server v3.10+ is required, Self-Signed SSL Certificates are not supported
- Server Versions Supported: Server v3.10+ is required, Self-Signed SSL Certificates are not yet supported
### Highlights
@@ -865,7 +57,7 @@ Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificate
## v1.0 Release
- Release Date: July 10, 2017
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not supported
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not yet supported
### Highlights

View File

@@ -1,5 +1,36 @@
# Code Contribution Guidelines
Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Native Mobile Apps" community channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to ask questions from community members and the Mattermost core team.
Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects.
Note: Community work won't start until October 31, and no community pull requests will be accepted before then.
### Review Process for this Repo
After following the steps in the [Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html), submitted pull requests go through the review process outlined below. We aim to start reviewing pull requests in this repo the week they are submitted, but the length of time to complete the process will vary depending on the pull request.
The one exception may be around release time, where the review process may take longer as the team focuses on our [release process](https://docs.mattermost.com/process/release-process.html).
#### `Stage 1: PM Review`
A Product Manager will review the pull request to make sure it:
1. Fits with our product roadmap
2. Works as expected
3. Meets UX guidelines
This step is sometimes skipped for bugs or small improvements with a ticket, but always happens for new features or pull requests without a related ticket.
The Product Manager may come back with some bugs or UI improvements to fix before the pull request moves on to the next stage.
#### `Stage 2: Dev Review`
Two developers will review the pull request and either give feedback or `+1` the PR.
Any comments will need to be addressed before the pull request moves on to the last stage.
- PRs that do not follow Style Guides cannot be merged
#### `Stage 3: Ready to Merge`
The review process is complete, and the pull request will be merged.
When you submit a pull request, it goes through a [code review process outlined here](https://developers.mattermost.com/contribute/getting-started/code-review/).

14
Jenkinsfile vendored
View File

@@ -1,14 +0,0 @@
pipeline {
agent any
stages {
stage('Test') {
steps {
echo 'assets/base/config.json'
sh 'cat assets/base/config.json'
sh 'touch .podinstall'
sh 'make test || exit 1'
}
}
}
}

View File

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

355
Makefile
View File

@@ -1,276 +1,163 @@
.PHONY: pre-run pre-build clean
.PHONY: check-style
.PHONY: i18n-extract-ci
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: test help
.PHONY: run run-ios run-android check-style test clean post-install start stop
.PHONY: check-ios-target build-ios
.PHONY: check-android-target prepare-android-build build-android
.PHONY: start-packager stop-packager
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
android_target := $(filter-out build-android,$(MAKECMDGOALS))
.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 dependencies using yarn
yarn install --pure-lockfile
touch $@
POD := $(shell which pod 2> /dev/null)
OS := $(shell sh -c 'uname -s 2>/dev/null')
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
MM_UTILITIES_DIR = ../mattermost-utilities
node_modules: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm install
npm-ci: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm ci
.podinstall:
ifeq ($(OS), Darwin)
ifdef POD
@echo Getting Cocoapods dependencies;
@cd ios && pod install;
else
@echo "Cocoapods is not installed https://cocoapods.org/"
@exit 1
endif
endif
@touch $@
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@mkdir -p dist
mkdir -p dist
@if [ -e dist/assets ] ; then \
rm -rf dist/assets; \
fi
@echo "Generating app assets"
@node scripts/make-dist-assets.js
node scripts/make-dist-assets.js
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
pre-run: .yarninstall dist/assets
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
run: run-ios
check-style: node_modules ## Runs eslint
@echo Checking for style guide compliance
@npm run check
start: | pre-run start-packager
clean: ## Cleans dependencies, previous builds and temp files
@echo Cleaning started
stop: stop-packager
@rm -rf node_modules
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@rm -rf ios/Pods
@rm -rf android/app/build
@echo Cleanup finished
post-install:
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
@# Need to copy custom RNCookieManagerIOS.m that fixes a crash when cookies does not have expiration date set
@cp ./native_modules/RNCookieManagerIOS.m node_modules/react-native-cookies/ios/RNCookieManagerIOS/RNCookieManagerIOS.m
@# Need to copy custom RNCNetInfo.m that checks for internet connectivity instead of reaching a host by default
@cp ./native_modules/RNCNetInfo.m node_modules/@react-native-community/netinfo/ios/RNCNetInfo.m
@rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
start: | pre-run ## Starts the React Native packager server
$(call start_packager)
stop: ## Stops the React Native packager server
$(call stop_packager)
check-device-ios:
@if ! [ $(shell which xcodebuild) ]; then \
run-ios: | start
@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:
@echo Running iOS app in development
npm run run-ios
open -a Simulator
run-android: | start prepare-android-build
@if ! [ $(ANDROID_HOME) ]; then \
echo "ANDROID_HOME is not set"; \
exit 1; \
fi
@if ! [ $(shell which adb 2> /dev/null) ]; then \
@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 \
ifneq ($(shell adb get-state),device)
echo "no android device or emulator is running"
exit 1;
endif
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
echo "watchman is not installed"; \
exit 1; \
fi
@echo Running Android app in development
npm run run-android
test: pre-run
npm test
check-style: .yarninstall
@echo Checking for style guide compliance
npm run check
clean:
@echo Cleaning app
yarn cache clean
rm -rf node_modules
rm -f .yarninstall
rm -rf dist
rm -rf ios/build
rm -rf android/app/build
post-install:
./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
@# Must remove the .babelrc for 0.42.0 to work correctly
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
sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
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
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 --reset-cache & echo $$! > server.PID; \
else \
echo React Native packager server already running; \
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
fi
stop-packager:
@echo Stopping React Native packager server
@if [ -e "server.PID" ] ; then \
kill -9 `cat server.PID` && rm server.PID; \
fi
check-ios-target:
ifneq ($(ios_target), $(filter $(ios_target), dev beta release))
@echo "Try running make build-ios TARGET\nWhere TARGET is one of dev, beta or release"
@exit 1
endif
do-build-ios:
@echo "Building ios $(ios_target) app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios $(ios_target)
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
check-android-target:
ifneq ($(android_target), $(filter $(android_target), dev alpha release))
@echo "Try running make build-android TARGET\nWhere TARGET is one of dev, beta or release"
@exit 1
endif
prepare-android-build:
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
@rm -rf ./node_modules/react-native-orientation/demo/
@cd android && ./gradlew clean
run: run-ios ## alias for run-ios
do-build-android:
@echo "Building android $(android_target) app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android $(android_target)
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
wait; \
else \
echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
react-native run-ios --simulator="${SIMULATOR}"; \
else \
react-native run-ios; \
fi; \
fi
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
wait; \
else \
echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
fi
alpha:
@:
build: | stop pre-build check-style i18n-extract-ci ## 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)
dev:
@:
beta:
@:
build-ios: | stop pre-build check-style i18n-extract-ci ## Builds the iOS app
$(call start_packager)
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
$(call stop_packager)
build-android: | stop pre-build check-style i18n-extract-ci prepare-android-build ## Build the Android app
$(call start_packager)
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
$(call stop_packager)
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
$(call start_packager)
@echo "Building unsigned iOS app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.ipa os_type:iOS
@rm -rf build-ios/
$(call stop_packager)
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
$(call start_packager)
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
@rm -rf build-ios/
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-simulator-x86_64.app.zip os_type:iOS
$(call stop_packager)
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
$(call start_packager)
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/unsigned/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@cd fastlane && bundle exec fastlane upload_file_to_s3 file:Mattermost-unsigned.apk os_type:Android
$(call stop_packager)
test: | pre-run check-style ## Runs tests
@npm test
build-pr: | can-build-pr stop pre-build check-style i18n-extract-ci ## 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
npm run mmjstool -- i18n extract-mobile
i18n-extract-ci:
mkdir -p tmp
cp assets/base/i18n/en.json tmp/en.json
mkdir -p tmp/fake-webapp-dir/i18n/
echo '{}' > tmp/fake-webapp-dir/i18n/en.json
npm run mmjstool -- i18n extract-mobile --webapp-dir tmp/fake-webapp-dir --mobile-dir .
diff tmp/en.json assets/base/i18n/en.json
rm -rf tmp
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@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
release:
@:

3009
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.

111
README.md
View File

@@ -1,17 +1,13 @@
# 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).
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
# How to Contribute
### Testing
@@ -19,25 +15,102 @@ We plan on releasing monthly updates with new features - check the [changelog](h
To help with testing app updates before they're released, you can:
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://testflight.apple.com/join/Q7Rx7K9P)
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
2. Install the `Mattermost Beta` app
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
- Device information
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
- Device information
- Repro steps
- Observed behavior (including screenshot / video when possible)
- Expected behavior
4. (Optional) [Sign up for our team site](https://pre-release.mattermost.com/signup_user_complete/?id=f1924a8db44ff3bb41c96424cdc20676)
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
- Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) to see what's new and discuss feedback with other contributors and the core team
### Contribute Code
1. Look in [GitHub issues](https://mattermost.com/pl/help-wanted-mattermost-mobile) 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
# Installing Dependencies
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
# Detailed configuration:
## Mac
- General requirements
- XCode 8.3
- Install required packages using homebrew:
```bash
$ brew install watchman
$ brew install yarn
```
- Clone repository and configure:
```bash
$ git clone git@github.com:mattermost/mattermost-mobile.git
$ cd mattermost-mobile
$ npm install
$ npm install -g react-native-cli
```
- Run application
```bash
$ make run
```
- Stop the packager server
```bash
$ make stop
```
## Linux:
- General requiriments:
- JDK 7 or greater
- Android SDK
- Virtualbox
- An Android emulator: Genymotion or Android emulator. If using genymotion ensure that it uses existing adb tools (Settings: "Use custom Android SDK Tools")
- Install watchman (do this globally):
```bash
$ git clone https://github.com/facebook/watchman.git
$ cd watchman
$ git checkout master
$ ./autogen.sh
$ ./configure make
$ sudo make install
```
Configure your kernel to accept a lot of file watches, using a command like:
```bash
$ sudo sysctl -w fs.inotify.max_user_watches=1048576
```
- Clone repository and configure:
```bash
$ git clone git@github.com:mattermost/mattermost-mobile.git
$ cd mattermost-mobile
$ npm install
$ npm install -g react-native-cli
```
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
`{
"DefaultServerUrl": "https://pre-release.mattermost.com"
}`
To use a local Mattermost server you will need to configure the "DefaultServerUrl" depending on the emulator you will use:
* IOs: "DefaultServerUrl": "http://localhost:8065"
* Android: "DefaultServerUrl": "http://10.0.2.2:3000"
* Genymotion: "DefaultServerUrl": "http://10.0.3.2:8065"
- Run application
- Start emulator
- Start react packager: `$ react-native start`
- Run in emulator: `$ react-native run-android`
# Frequently Asked Questions
@@ -45,13 +118,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

@@ -1,3 +1,5 @@
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
@@ -9,9 +11,8 @@
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
@@ -19,7 +20,7 @@ for jarfile in glob(['libs/*.jar']):
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
@@ -27,39 +28,39 @@ for aarfile in glob(['libs/*.aar']):
)
android_library(
name = "all-libs",
exported_deps = lib_deps,
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = "build_config",
package = "com.mattermost.rnbeta",
name = 'build_config',
package = 'com.mattermost.rnbeta',
)
android_resource(
name = "res",
package = "com.mattermost.rnbeta",
res = "src/main/res",
name = 'res',
res = 'src/main/res',
package = 'com.mattermost.rnbeta',
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)

View File

@@ -33,13 +33,6 @@ import com.android.build.OutputFile
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
@@ -65,31 +58,16 @@ import com.android.build.OutputFile
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
* nodeExecutableAndArgs: ["node"]
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "metro.config.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"
}
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
@@ -106,28 +84,18 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
pickFirst '**/libjsc.so'
pickFirst '**/libc++_shared.so'
}
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionName "1.23.1"
versionCode 234
multiDexEnabled = true
minSdkVersion 16
targetSdkVersion 23
versionCode 49
versionName "1.2.0"
multiDexEnabled true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
abiFilters "armeabi-v7a", "x86"
}
}
@@ -146,7 +114,7 @@ android {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
include "armeabi-v7a", "x86"
}
}
buildTypes {
@@ -159,18 +127,13 @@ android {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
matchingFallbacks = ['debug', 'release']
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
def versionCodes = ["armeabi-v7a":1, "x86":2]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
@@ -178,77 +141,27 @@ android {
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
repositories {
maven {
url 'https://maven.google.com'
}
}
configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
}
}
}
dependencies {
// Make sure to put android-jsc at the top
implementation "org.webkit:android-jsc-intl:r241213"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:percent:28.0.0'
implementation "com.google.firebase:firebase-messaging:17.3.0"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
implementation project(':react-native-doc-viewer')
implementation project(':react-native-video')
implementation project(':react-native-navigation')
implementation project(':react-native-image-picker')
implementation project(':react-native-device-info')
implementation project(':reactnativenotifications')
implementation project(':react-native-cookies')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-vector-icons')
implementation project(':react-native-svg')
implementation project(':react-native-local-auth')
implementation project(':jail-monkey')
implementation project(':react-native-youtube')
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':rn-fetch-blob')
implementation project(':react-native-webview')
implementation project(':react-native-gesture-handler')
implementation project(':@react-native-community_async-storage')
implementation project(':@react-native-community_netinfo')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.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')
}
// Run this once to be able to run the application with BUCK
@@ -257,5 +170,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

View File

@@ -26,7 +26,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -57,7 +57,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -88,7 +88,7 @@
],
"services": {
"analytics_service": {
"status": 2
"status": 1
},
"appinvite_service": {
"status": 1,
@@ -101,4 +101,4 @@
}
],
"configuration_version": "1"
}
}

View File

@@ -15,3 +15,52 @@
#-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.**
# 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,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

View File

@@ -1,65 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mattermost.rnbeta">
package="com.mattermost.rnbeta"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="com.google.android.c2dm.permission.SEND" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="false"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme"
android:installLocation="auto"
android:networkSecurityConfig="@xml/network_security_config"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />
<receiver android:name=".NotificationReplyBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:resizeableActivity="true"/>
<activity
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<!-- for sharing-->
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<receiver android:name=".NotificationDismissReceiver" />
</application>
</manifest>

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,35 +1,20 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Person;
import android.app.Person.Builder;
import android.app.RemoteInput;
import android.content.Intent;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
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 android.app.Notification;
import android.app.NotificationManager;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
@@ -38,56 +23,22 @@ import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
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";
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
private static int badgeCount = 0;
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
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) {
if (notificationId != -1) {
Object objCount = channelIdToNotificationCount.get(channelId);
Integer count = -1;
if (objCount != null) {
count = (Integer)objCount;
}
String channelId = String.valueOf(notificationId);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.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);
}
}
@@ -96,13 +47,7 @@ public class CustomPushNotification extends PushNotification {
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String type = data.getString("type");
final String ackId = data.getString("ack_id");
int notificationId = MESSAGE_NOTIFICATION_ID;
if (ackId != null) {
notificationReceiptDelivery(ackId, type);
}
if (channelId != null) {
notificationId = channelId.hashCode();
Object objCount = channelIdToNotificationCount.get(channelId);
@@ -113,22 +58,14 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.put(channelId, count);
Object bundleArray = channelIdToNotification.get(channelId);
List list = null;
ArrayList list = null;
if (bundleArray == null) {
list = Collections.synchronizedList(new ArrayList(0));
list = new ArrayList();
} else {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
if (!"clear".equals(type)) {
String senderName = getSenderName(data.getString("sender_name"), data.getString("channel_name"), data.getString("message"));
data.putLong("time", new Date().getTime());
data.putString("sender_name", senderName);
data.putString("sender_id", data.getString("sender_id"));
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
list = (ArrayList)bundleArray;
}
list.add(data);
channelIdToNotification.put(channelId, list);
}
if ("clear".equals(type)) {
@@ -147,17 +84,12 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
digestNotification();
clearAllNotifications();
}
@Override
protected void postNotification(int id, Notification notification) {
boolean force = false;
Bundle bundle = notification.extras;
if (bundle != null) {
force = bundle.getBoolean("localTest");
}
if (!mAppLifecycleFacade.isAppVisible() || force) {
if (!mAppLifecycleFacade.isAppVisible()) {
super.postNotification(id, notification);
}
}
@@ -166,57 +98,23 @@ public class CustomPushNotification extends PushNotification {
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
channel.setShowBadge(true);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification.setChannelId(CHANNEL_ID);
}
final Notification.Builder notification = super.getNotificationBuilder(intent);
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
String channelId = bundle.getString("channel_id");
String channelName = bundle.getString("channel_name");
String senderName = bundle.getString("sender_name");
String senderId = bundle.getString("sender_id");
String postId = bundle.getString("post_id");
String badge = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
String title = null;
if (version != null && version.equals("v2")) {
title = channelName;
} else {
title = bundle.getString("title");
}
if (android.text.TextUtils.isEmpty(title)) {
String title = bundle.getString("title");
if (title == null) {
ApplicationInfo appInfo = mContext.getApplicationInfo();
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
Bundle b = bundle.getBundle("userInfo");
if (b == null) {
b = new Bundle();
}
b.putString("channel_id", channelId);
notification.addExtras(b);
int notificationId = bundle.getString("channel_id").hashCode();
String channelId = bundle.getString("channel_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 smallIconResId;
int largeIconResId;
@@ -241,138 +139,60 @@ public class CustomPushNotification extends PushNotification {
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
}
if (badge != null) {
int badgeCount = Integer.parseInt(badge);
CustomPushNotification.badgeCount = badgeCount;
notification.setNumber(badgeCount);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
if (numberString != null) {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
if (android.text.TextUtils.isEmpty(senderName)) {
senderName = getSenderName(senderName, channelName, bundle.getString("message"));
}
String personId = senderId;
if (!android.text.TextUtils.isEmpty(channelName)) {
personId = channelId;
}
Notification.MessagingStyle messagingStyle;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle = new Notification.MessagingStyle("");
} else {
Person sender = new Person.Builder()
.setKey(senderId)
.setName("")
.build();
messagingStyle = new Notification.MessagingStyle(sender);
}
if (title != null && (!title.startsWith("@") || channelName != senderName)) {
messagingStyle
.setConversationTitle(title);
}
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
list.add(bundle);
}
int listCount = list.size() - 1;
for (int i = listCount; i >= 0; i--) {
Bundle data = list.get(i);
String message = data.getString("message");
String previousPersonName = getSenderName(data.getString("sender_name"), channelName, message);
String previousPersonId = data.getString("sender_id");
if (title == null || !android.text.TextUtils.isEmpty(previousPersonName)) {
message = removeSenderFromMessage(previousPersonName, channelName, message);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
messagingStyle.addMessage(message, data.getLong("time"), previousPersonName);
} else {
Person sender = new Person.Builder()
.setKey(previousPersonId)
.setName(previousPersonName)
.build();
messagingStyle.addMessage(message, data.getLong("time"), sender);
}
int numMessages = 0;
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
numMessages = (Integer)objCount;
}
notification
.setContentIntent(intent)
.setGroupSummary(true)
.setStyle(messagingStyle)
.setSmallIcon(smallIconResId)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true);
.setPriority(Notification.PRIORITY_HIGH);
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();
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
for (Bundle data : list){
style.addLine(data.getString("message"));
}
style.setBigContentTitle(title);
notification.setStyle(style)
.setContentTitle(summaryTitle);
// .setNumber(numMessages);
}
// Let's add a delete intent when the notification is dismissed
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
Intent delIntent = new Intent(mContext, NotificationDismissReceiver.class);
delIntent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, delIntent, 0);
notification.setDeleteIntent(deleteIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notification.setGroup(GROUP_KEY_MESSAGES);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
.build();
Notification.Action replyAction = new Notification.Action.Builder(
R.drawable.ic_notif_action_reply, "Reply", replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
notification.setLargeIcon(largeIconBitmap);
}
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (soundUri != "none") {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// use the system default for vibration
notification.setDefaults(Notification.DEFAULT_VIBRATE);
}
boolean blink = notificationPreferences.getShouldBlink();
if (blink) {
notification.setLights(Color.CYAN, 500, 500);
if (subText != null) {
notification.setSubText(subText);
}
return notification;
@@ -382,46 +202,17 @@ public class CustomPushNotification extends PushNotification {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
public static Integer getMessageCountInChannel(String channelId) {
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
return (Integer)objCount;
}
return 1;
}
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");
final String numberString = data.getString("badge");
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
}
private String getSenderName(String senderName, String channelName, String message) {
if (senderName != null) {
return senderName;
} else if (channelName != null && channelName.startsWith("@")) {
return channelName;
String numberString = data.getString("badge");
if (numberString != null) {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
String name = message.split(":")[0];
if (name != message) {
return name;
}
return " ";
}
private String removeSenderFromMessage(String senderName, String channelName, String message) {
String sender = String.format("%s", getSenderName(senderName, channelName, message));
return message.replaceFirst(sender, "").replaceFirst(": ", "").trim();
}
private void notificationReceiptDelivery(String ackId, String type) {
ReceiptDelivery.send(context, ackId, type);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}

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,28 +1,63 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.reactnativenavigation.controllers.SplashActivity;
import com.reactnativenavigation.NavigationActivity;
import java.lang.ref.WeakReference;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.graphics.Color;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;
import android.util.TypedValue;
public class MainActivity extends SplashActivity {
private static ImageView imageView;
private static WeakReference<MainActivity> wr_activity;
protected static MainActivity getActivity() {
return wr_activity.get();
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
// @Override
// protected String getMainComponentName() {
// return "Mattermost";
// }
public class MainActivity extends NavigationActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.launch_screen);
public LinearLayout createSplashLayout() {
wr_activity = new WeakReference<>(this);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
Context context = getActivity();
final int drawableId = getImageId();
/**
* Reference: https://stackoverflow.com/questions/7944338/resume-last-activity-when-launcher-icon-is-clicked
* 1. Open app from launcher/appDrawer
* 2. Go home
* 3. Send notification and open
* 4. It creates a new Activity and Destroys the old
* 5. Causing an unnecessary app restart
* 6. This solution short-circuits the restart
*/
if (!isTaskRoot()) {
finish();
return;
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.CENTER);
LinearLayout view = new LinearLayout(this);
view.setBackgroundColor(Color.parseColor("#FFFFFF"));
view.setGravity(Gravity.CENTER);
view.addView(imageView);
return view;
}
private static int getImageId() {
int drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getClass().getPackage().getName());
if (drawableId == 0) {
drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getPackageName());
}
return drawableId;
}
}

View File

@@ -1,98 +1,40 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.app.Application;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.content.Context;
import android.content.RestrictionsManager;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.util.HashMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.facebook.react.ReactApplication;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
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.mattermost.share.ShareModule;
import com.learnium.RNDeviceInfo.RNDeviceModule;
import com.imagepicker.ImagePickerModule;
import com.psykar.cookiemanager.CookieManagerModule;
import com.oblador.vectoricons.VectorIconsModule;
import com.wix.reactnativenotifications.RNNotificationsModule;
import io.tradle.react.LocalAuthModule;
import com.gantix.JailMonkey.JailMonkeyModule;
import com.RNFetchBlob.RNFetchBlob;
import io.sentry.RNSentryModule;
import io.sentry.RNSentryEventEmitter;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule;
import com.inprogress.reactnativeyoutube.YouTubeStandaloneModule;
import com.reactlibrary.RNReactNativeDocViewerModule;
import com.reactnativedocumentpicker.DocumentPicker;
import com.oblador.keychain.KeychainModule;
import com.reactnativecommunity.asyncstorage.AsyncStorageModule;
import com.reactnativecommunity.netinfo.NetInfoModule;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.imagepicker.ImagePickerPackage;
import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.react.NavigationReactNativeHost;
import com.reactnativenavigation.react.ReactGateway;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.facebook.react.ReactPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMarker;
import com.facebook.react.bridge.ReactMarkerConstants;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.soloader.SoLoader;
import java.util.Arrays;
import java.util.List;
import com.mattermost.share.RealPathUtil;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public static MainApplication instance;
public Boolean sharedExtensionIsOpened = false;
public long APP_START_TIME;
public long RELOAD;
public long CONTENT_APPEARED;
public long PROCESS_PACKAGES_START;
public long PROCESS_PACKAGES_END;
private Bundle mManagedConfig = null;
@Override
protected ReactGateway createReactGateway() {
ReactNativeHost host = new NavigationReactNativeHost(this, isDebug(), createAdditionalReactPackages()) {
@Override
protected String getJSMainModuleName() {
return "index";
}
};
return new ReactGateway(this, isDebug(), host);
}
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
@Override
public boolean isDebug() {
@@ -105,95 +47,18 @@ public class MainApplication extends NavigationApplication implements INotificat
// Add the packages you require here.
// No need to add RnnPackage and MainReactPackage
return Arrays.<ReactPackage>asList(
new TurboReactPackage() {
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
switch (name) {
case "MattermostShare":
return new ShareModule(instance, reactContext);
case "RNDeviceInfo":
return new RNDeviceModule(reactContext, false);
case "ImagePickerManager":
return new ImagePickerModule(reactContext, R.style.DefaultExplainingPermissionsTheme);
case "RNCookieManagerAndroid":
return new CookieManagerModule(reactContext);
case "RNVectorIconsModule":
return new VectorIconsModule(reactContext);
case "WixRNNotifications":
return new RNNotificationsModule(instance, reactContext);
case "RNLocalAuth":
return new LocalAuthModule(reactContext);
case "JailMonkey":
return new JailMonkeyModule(reactContext);
case "RNFetchBlob":
return new RNFetchBlob(reactContext);
case "MattermostManaged":
return MattermostManagedModule.getInstance(reactContext);
case "NotificationPreferences":
return NotificationPreferencesModule.getInstance(instance, reactContext);
case "RNTextInputReset":
return new RNTextInputResetModule(reactContext);
case "RNSentry":
return new RNSentryModule(reactContext);
case "RNSentryEventEmitter":
return new RNSentryEventEmitter(reactContext);
case "ReactNativeExceptionHandler":
return new ReactNativeExceptionHandlerModule(reactContext);
case "YouTubeStandaloneModule":
return new YouTubeStandaloneModule(reactContext);
case "RNReactNativeDocViewer":
return new RNReactNativeDocViewerModule(reactContext);
case "RNDocumentPicker":
return new DocumentPicker(reactContext);
case "RNKeychainManager":
return new KeychainModule(reactContext);
case AsyncStorageModule.NAME:
return new AsyncStorageModule(reactContext);
case NetInfoModule.NAME:
return new NetInfoModule(reactContext);
default:
throw new IllegalArgumentException("Could not find module " + name);
}
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("NotificationPreferences", new ReactModuleInfo("NotificationPreferences", "com.mattermost.rnbeta.NotificationPreferencesModule", false, false, false, false, false));
map.put("RNTextInputReset", new ReactModuleInfo("RNTextInputReset", "com.mattermost.rnbeta.RNTextInputResetModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("RNDeviceInfo", new ReactModuleInfo("RNDeviceInfo", "com.learnium.RNDeviceInfo.RNDeviceModule", false, false, true, false, false));
map.put("ImagePickerManager", new ReactModuleInfo("ImagePickerManager", "com.imagepicker.ImagePickerModule", false, false, false, false, false));
map.put("RNCookieManagerAndroid", new ReactModuleInfo("RNCookieManagerAndroid", "com.psykar.cookiemanager.CookieManagerModule", false, false, false, false, false));
map.put("RNVectorIconsModule", new ReactModuleInfo("RNVectorIconsModule", "com.oblador.vectoricons.VectorIconsModule", false, false, false, false, false));
map.put("WixRNNotifications", new ReactModuleInfo("WixRNNotifications", "com.wix.reactnativenotifications.RNNotificationsModule", false, false, false, false, false));
map.put("RNLocalAuth", new ReactModuleInfo("RNLocalAuth", "io.tradle.react.LocalAuthModule", false, false, false, false, false));
map.put("JailMonkey", new ReactModuleInfo("JailMonkey", "com.gantix.JailMonkey.JailMonkeyModule", false, false, true, false, false));
map.put("RNFetchBlob", new ReactModuleInfo("RNFetchBlob", "com.RNFetchBlob.RNFetchBlob", false, false, true, false, false));
map.put("RNSentry", new ReactModuleInfo("RNSentry", "com.sentry.RNSentryModule", false, false, true, false, false));
map.put("RNSentryEventEmitter", new ReactModuleInfo("RNSentryEventEmitter", "com.sentry.RNSentryEventEmitter", false, false, true, false, false));
map.put("ReactNativeExceptionHandler", new ReactModuleInfo("ReactNativeExceptionHandler", "com.masteratul.exceptionhandler.ReactNativeExceptionHandlerModule", false, false, false, false, false));
map.put("YouTubeStandaloneModule", new ReactModuleInfo("YouTubeStandaloneModule", "com.inprogress.reactnativeyoutube.YouTubeStandaloneModule", false, false, false, false, false));
map.put("RNReactNativeDocViewer", new ReactModuleInfo("RNReactNativeDocViewer", "com.reactlibrary.RNReactNativeDocViewerModule", false, false, false, false, false));
map.put("RNDocumentPicker", new ReactModuleInfo("RNDocumentPicker", "com.reactnativedocumentpicker.DocumentPicker", false, false, false, false, false));
map.put("RNKeychainManager", new ReactModuleInfo("RNKeychainManager", "com.oblador.keychain.KeychainModule", false, false, true, false, false));
map.put(AsyncStorageModule.NAME, new ReactModuleInfo(AsyncStorageModule.NAME, "com.reactnativecommunity.asyncstorage.AsyncStorageModule", false, false, false, false, false));
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
return map;
}
};
}
},
new RNCWebViewPackage(),
new ImagePickerPackage(),
new RNBottomSheetPackage(),
new RNDeviceInfo(),
new CookieManagerPackage(),
new VectorIconsPackage(),
new SvgPackage(),
new LinearGradientPackage(),
new ReactVideoPackage(),
new RNGestureHandlerPackage()
new OrientationPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
new MattermostManagedPackage()
);
}
@@ -201,18 +66,12 @@ public class MainApplication extends NavigationApplication implements INotificat
public void onCreate() {
super.onCreate();
instance = this;
registerActivityLifecycleCallbacks(new ManagedActivityLifecycleCallbacks());
// Delete any previous temp files created by the app
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
RealPathUtil.deleteTempFiles(tempFolder);
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
// Create an object of the custom facade impl
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
// Attach it to react-native-navigation
setActivityCallbacks(notificationsLifecycleFacade);
SoLoader.init(this, /* native exopackage */ false);
// Uncomment to listen to react markers for build that has telemetry enabled
// addReactMarkerListener();
}
@Override
@@ -220,91 +79,9 @@ public class MainApplication extends NavigationApplication implements INotificat
return new CustomPushNotification(
context,
bundle,
defaultFacade,
notificationsLifecycleFacade, // Instead of defaultFacade!!!
defaultAppLaunchHelper,
new JsIOHelper()
);
}
@Override
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
public ReactContext getRunningReactContext() {
final ReactGateway reactGateway = getReactGateway();
if (reactGateway == null) {
return null;
}
return reactGateway
.getReactNativeHost()
.getReactInstanceManager()
.getCurrentReactContext();
}
public synchronized Bundle loadManagedConfig(Context ctx) {
if (ctx != null) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) ctx.getSystemService(Context.RESTRICTIONS_SERVICE);
mManagedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
return mManagedConfig;
}
return null;
}
return null;
}
public synchronized Bundle getManagedConfig() {
if (mManagedConfig!= null && mManagedConfig.size() > 0) {
return mManagedConfig;
}
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
return loadManagedConfig(ctx);
}
return null;
}
private void addReactMarkerListener() {
ReactMarker.addListener(new ReactMarker.MarkerListener() {
@Override
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
APP_START_TIME = System.currentTimeMillis();
RELOAD = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
PROCESS_PACKAGES_START = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
PROCESS_PACKAGES_END = System.currentTimeMillis();
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
CONTENT_APPEARED = System.currentTimeMillis();
ReactContext ctx = getRunningReactContext();
if (ctx != null) {
WritableMap map = Arguments.createMap();
map.putDouble("appReload", RELOAD);
map.putDouble("appContentAppeared", CONTENT_APPEARED);
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("nativeMetrics", map);
}
}
}
});
}
}

View File

@@ -1,145 +0,0 @@
package com.mattermost.rnbeta;
import android.os.Bundle;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.IntentFilter;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.util.ArraySet;
import android.util.Log;
import java.util.Set;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class ManagedActivityLifecycleCallbacks implements ActivityLifecycleCallbacks {
private static final String TAG = ManagedActivityLifecycleCallbacks.class.getSimpleName();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context ctx, Intent intent) {
if (ctx != null) {
Bundle managedConfig = MainApplication.instance.loadManagedConfig(ctx);
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i(TAG, "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled() && activity != null) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (managedConfig != null && activity != null) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@Override
public void onActivityResumed(Activity activity) {
ReactContext ctx = MainApplication.instance.getRunningReactContext();
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (ctx != null) {
Bundle newConfig = MainApplication.instance.loadManagedConfig(ctx);
if (!equalBundles(newConfig, managedConfig)) {
Log.i(TAG, "onResumed Managed Configuration Changed");
sendConfigChanged(newConfig);
}
}
}
@Override
public void onActivityStopped(Activity activity) {
Bundle managedConfig = MainApplication.instance.getManagedConfig();
if (managedConfig != null) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
private void sendConfigChanged(Bundle config) {
WritableMap result = Arguments.createMap();
if (config != null) {
result = Arguments.fromBundle(config);
}
ReactContext ctx = MainApplication.instance.getRunningReactContext();
if (ctx != null) {
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("managedConfigDidChange", result);
}
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

View File

@@ -1,41 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import java.util.ArrayList;
import java.util.HashMap;
import com.facebook.react.bridge.ReactApplicationContext;
import com.oblador.keychain.KeychainModule;
import com.mattermost.react_native_interface.ResolvePromise;
import com.mattermost.react_native_interface.AsyncStorageHelper;
import com.mattermost.react_native_interface.KeysReadableArray;
public class MattermostCredentialsHelper {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
final KeychainModule keychainModule = new KeychainModule(context);
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
final ArrayList<String> keys = new ArrayList<String>(1);
keys.add(CURRENT_SERVER_URL);
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {
return keys.size();
}
@Override
public String getString(int index) {
return keys.get(index);
}
};
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
keychainModule.getGenericPasswordForOptions(serverUrl, promise);
}
}

View File

@@ -1,11 +1,8 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
@@ -14,7 +11,6 @@ import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
private static MattermostManagedModule instance;
@@ -54,46 +50,16 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getConfig(final Promise promise) {
try {
Bundle config = MainApplication.instance.getManagedConfig();
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
if (config != null) {
Object result = Arguments.fromBundle(config);
promise.resolve(result);
} else {
promise.resolve(Arguments.createMap());
throw new Exception("The MDM vendor has not sent any Managed configuration");
}
} catch (Exception e) {
promise.resolve(Arguments.createMap());
promise.reject("no managed configuration", e);
}
}
@ReactMethod
// Close the current activity and open the security settings.
public void goToSecuritySettings() {
Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getReactApplicationContext().startActivity(intent);
getCurrentActivity().finish();
System.exit(0);
}
@ReactMethod
public void isRunningInSplitView(final Promise promise) {
WritableMap result = Arguments.createMap();
Activity current = getCurrentActivity();
if (current != null) {
result.putBoolean("isSplitView", current.isInMultiWindowMode());
} else {
result.putBoolean("isSplitView", false);
}
promise.resolve(result);
}
@ReactMethod
public void quitApp() {
getCurrentActivity().finish();
System.exit(0);
}
}

View File

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

View File

@@ -0,0 +1,14 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver;
public class NotificationDismissReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(notificationId);
}
}

View File

@@ -1,26 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import android.util.Log;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
mContext = getApplicationContext();
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
Log.i("ReactNative", "Dismiss notification");
}
}

View File

@@ -1,57 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.SharedPreferences;
public class NotificationPreferences {
private static NotificationPreferences instance;
public final String SHARED_NAME = "NotificationPreferences";
public final String SOUND_PREF = "NotificationSound";
public final String VIBRATE_PREF = "NotificationVibrate";
public final String BLINK_PREF = "NotificationLights";
private SharedPreferences mSharedPreferences;
private NotificationPreferences(Context context) {
mSharedPreferences = context.getSharedPreferences(SHARED_NAME, Context.MODE_PRIVATE);
}
public static NotificationPreferences getInstance(Context context) {
if (instance == null) {
instance = new NotificationPreferences(context);
}
return instance;
}
public String getNotificationSound() {
return mSharedPreferences.getString(SOUND_PREF, null);
}
public boolean getShouldVibrate() {
return mSharedPreferences.getBoolean(VIBRATE_PREF, true);
}
public boolean getShouldBlink() {
return mSharedPreferences.getBoolean(BLINK_PREF, false);
}
public void setNotificationSound(String soundUri) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(SOUND_PREF, soundUri);
editor.commit();
}
public void setShouldVibrate(boolean vibrate) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(VIBRATE_PREF, vibrate);
editor.commit();
}
public void setShouldBlink(boolean blink) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(BLINK_PREF, blink);
editor.commit();
}
}

View File

@@ -1,136 +0,0 @@
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;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
private static NotificationPreferencesModule instance;
private final MainApplication mApplication;
private NotificationPreferences mNotificationPreference;
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
Context context = mApplication.getApplicationContext();
mNotificationPreference = NotificationPreferences.getInstance(context);
}
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
if (instance == null) {
instance = new NotificationPreferencesModule(application, reactContext);
}
return instance;
}
public static NotificationPreferencesModule getInstance() {
return instance;
}
@Override
public String getName() {
return "NotificationPreferences";
}
@ReactMethod
public void getPreferences(final Promise promise) {
try {
Context context = mApplication.getApplicationContext();
RingtoneManager manager = new RingtoneManager(context);
manager.setType(RingtoneManager.TYPE_NOTIFICATION);
Cursor cursor = manager.getCursor();
WritableMap result = Arguments.createMap();
WritableArray sounds = Arguments.createArray();
while (cursor.moveToNext()) {
String notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
String notificationId = cursor.getString(RingtoneManager.ID_COLUMN_INDEX);
String notificationUri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX);
WritableMap map = Arguments.createMap();
map.putString("name", notificationTitle);
map.putString("uri", (notificationUri + "/" + notificationId));
sounds.pushMap(map);
}
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
if (defaultUri != null) {
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
}
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());
result.putArray("sounds", sounds);
promise.resolve(result);
} catch (Exception e) {
promise.reject("no notification sounds found", e);
}
}
@ReactMethod
public void previewSound(String url) {
Context context = mApplication.getApplicationContext();
Uri uri = Uri.parse(url);
Ringtone r = RingtoneManager.getRingtone(context, uri);
r.play();
}
@ReactMethod
public void setNotificationSound(String soundUri) {
mNotificationPreference.setNotificationSound(soundUri);
}
@ReactMethod
public void setShouldVibrate(boolean vibrate) {
mNotificationPreference.setShouldVibrate(vibrate);
}
@ReactMethod
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,154 +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.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) {
final CharSequence message = getReplyMessage(intent);
if (message == null) {
return;
}
mContext = context;
bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
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 postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
if (android.text.TextUtils.isEmpty(rootId)) {
rootId = postId;
}
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,228 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager.LayoutParams;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.controllers.ActivityCallbacks;
import com.reactnativenavigation.react.ReactGateway;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade {
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private static NotificationsLifecycleFacade instance;
private Bundle managedConfig = null;
private Activity mVisibleActivity;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
if (mVisibleActivity != null) {
// Get the current configuration bundle
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) mVisibleActivity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i("ReactNative", "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
public static NotificationsLifecycleFacade getInstance() {
if (instance == null) {
instance = new NotificationsLifecycleFacade();
}
return instance;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig!= null && managedConfig.size() > 0) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@Override
public void onActivityResumed(Activity activity) {
switchToVisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
if (!equalBundles(newConfig ,managedConfig)) {
Log.i("ReactNative", "onResumed Managed Configuration Changed");
managedConfig = newConfig;
sendConfigChanged(managedConfig);
}
}
}
@Override
public void onActivityPaused(Activity activity) {
switchToInvisible(activity);
}
@Override
public void onActivityStopped(Activity activity) {
switchToInvisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
public void onActivityDestroyed(Activity activity) {
switchToInvisible(activity);
}
@Override
public boolean isReactInitialized() {
return NavigationApplication.instance.isReactContextInitialized();
}
@Override
public ReactContext getRunningReactContext() {
final ReactGateway reactGateway = NavigationApplication.instance.getReactGateway();
if (reactGateway == null || !reactGateway.isInitialized()) {
return null;
}
return reactGateway.getReactContext();
}
@Override
public boolean isAppVisible() {
return mVisibleActivity != null;
}
@Override
public synchronized void addVisibilityListener(AppVisibilityListener listener) {
mListeners.add(listener);
}
@Override
public synchronized void removeVisibilityListener(AppVisibilityListener listener) {
mListeners.remove(listener);
}
private synchronized void switchToVisible(Activity activity) {
if (mVisibleActivity == null) {
mVisibleActivity = activity;
Log.v(TAG, "Activity is now visible ("+activity+")");
for (AppVisibilityListener listener : mListeners) {
listener.onAppVisible();
}
}
}
private synchronized void switchToInvisible(Activity activity) {
if (mVisibleActivity == activity) {
mVisibleActivity = null;
Log.v(TAG, "Activity is now NOT visible ("+activity+")");
for (AppVisibilityListener listener : mListeners) {
listener.onAppNotVisible();
}
}
}
public synchronized void LoadManagedConfig(Activity activity) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
public synchronized Bundle getManagedConfig() {
if (managedConfig!= null && managedConfig.size() > 0) {
return managedConfig;
}
if (mVisibleActivity != null) {
LoadManagedConfig(mVisibleActivity);
return managedConfig;
}
return null;
}
public void sendConfigChanged(Bundle config) {
Object result = Arguments.fromBundle(config);
getRunningReactContext().
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

View File

@@ -1,42 +0,0 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import android.content.Context;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
public class RNTextInputResetModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
public RNTextInputResetModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "RNTextInputReset";
}
// https://github.com/facebook/react-native/pull/12462#issuecomment-298812731
@ReactMethod
public void resetKeyboardInput(final int reactTagToReset) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
imm.restartInput(viewToReset);
}
}
});
}
}

View File

@@ -1,95 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import java.lang.System;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.react_native_interface.ResolvePromise;
public class ReceiptDelivery {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
public static void send (Context context, final String ackId, final String type) {
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
MattermostCredentialsHelper.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (value instanceof Boolean && !(Boolean)value) {
return;
}
WritableMap map = (WritableMap) value;
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
if (serverUrl.isEmpty()) {
String[] credentials = token.split(",[ ]*");
if (credentials.length == 2) {
token = credentials[0];
serverUrl = credentials[1];
}
}
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token));
execute(serverUrl, token, ackId, type);
}
}
});
}
protected static void execute(String serverUrl, String token, String ackId, String type) {
if (token == null || serverUrl == null) {
return;
}
JSONObject json;
long receivedAt = System.currentTimeMillis();
try {
json = new JSONObject();
json.put("id", ackId);
json.put("received_at", receivedAt);
json.put("platform", "android");
json.put("type", type);
} catch (JSONException e) {
Log.e("ReactNative", "Receipt delivery failed to build json payload");
return;
}
final HttpUrl url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, json.toString());
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
try {
client.newCall(request).execute();
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
}
}
}
}

View File

@@ -1,233 +0,0 @@
package com.mattermost.share;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.util.Log;
import android.text.TextUtils;
import android.os.ParcelFileDescriptor;
import java.io.*;
import java.nio.channels.FileChannel;
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
public class RealPathUtil {
public static String getRealPathFromURI(final Context context, final Uri uri) {
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) {
// DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
try {
return getPathFromSavingTempFile(context, uri);
} catch (NumberFormatException e) {
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
return null;
}
}
} else if (isMediaDocument(uri)) {
// MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
if ("content".equalsIgnoreCase(uri.getScheme())) {
// MediaStore (and general)
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
// Try save to tmp file, and return tmp file path
return getPathFromSavingTempFile(context, uri);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
File tmpFile;
String fileName = null;
// Try and get the filename from the Uri
try {
Cursor returnCursor =
context.getContentResolver().query(uri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
fileName = returnCursor.getString(nameIndex);
} catch (Exception e) {
// just continue to get the filename with the last segment of the path
}
try {
if (fileName == null) {
fileName = uri.getLastPathSegment().toString().trim();
}
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} catch (IOException ex) {
return null;
}
return tmpFile.getAbsolutePath();
}
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
public static String getExtension(String uri) {
if (uri == null) {
return null;
}
int dot = uri.lastIndexOf(".");
if (dot >= 0) {
return uri.substring(dot);
} else {
// No extension.
return "";
}
}
public static String getMimeType(File file) {
String extension = getExtension(file.getName());
if (extension.length() > 0)
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
return "application/octet-stream";
}
public static String getMimeType(String filePath) {
File file = new File(filePath);
return getMimeType(file);
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
try {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
} catch (Exception e) {
return "application/octet-stream";
}
}
public static void deleteTempFiles(final File dir) {
try {
if (dir.isDirectory()) {
deleteRecursive(dir);
}
} catch (Exception e) {
// do nothing
}
}
private static void deleteRecursive(File fileOrDirectory) {
if (fileOrDirectory.isDirectory())
for (File child : fileOrDirectory.listFiles())
deleteRecursive(child);
fileOrDirectory.delete();
}
}

View File

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

View File

@@ -1,257 +0,0 @@
package com.mattermost.share;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.Arguments;
import com.mattermost.rnbeta.MainApplication;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import java.io.File;
import java.util.ArrayList;
import javax.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ShareModule extends ReactContextBaseJavaModule {
private final OkHttpClient client = new OkHttpClient();
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private final MainApplication mApplication;
public ShareModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
}
private File tempFolder;
@Override
public String getName() {
return "MattermostShare";
}
@ReactMethod
public void clear() {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
Intent intent = currentActivity.getIntent();
intent.setAction("");
intent.removeExtra(Intent.EXTRA_TEXT);
intent.removeExtra(Intent.EXTRA_STREAM);
}
}
@Nullable
@Override
public Map<String, Object> getConstants() {
HashMap<String, Object> constants = new HashMap<>(1);
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
mApplication.sharedExtensionIsOpened = false;
return constants;
}
@ReactMethod
public void close(ReadableMap data) {
this.clear();
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
currentActivity.finish();
}
if (data != null && data.hasKey("url")) {
ReadableArray files = data.getArray("files");
String serverUrl = data.getString("url");
String token = data.getString("token");
JSONObject postData = buildPostObject(data);
if (files.size() > 0) {
uploadFiles(serverUrl, token, files, postData);
} else {
try {
post(serverUrl, token, postData);
} catch (IOException e) {
e.printStackTrace();
}
}
}
RealPathUtil.deleteTempFiles(this.tempFolder);
}
@ReactMethod
public void data(Promise promise) {
promise.resolve(processIntent());
}
@ReactMethod
public void getFilePath(String filePath, Promise promise) {
Activity currentActivity = getCurrentActivity();
WritableMap map = Arguments.createMap();
if (currentActivity != null) {
Uri uri = Uri.parse(filePath);
String path = RealPathUtil.getRealPathFromURI(currentActivity, uri);
if (path != null) {
String text = "file://" + path;
map.putString("filePath", text);
}
}
promise.resolve(map);
}
public WritableArray processIntent() {
WritableMap map = Arguments.createMap();
WritableArray items = Arguments.createArray();
String text = "";
String type = "";
String action = "";
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
if (type == null) {
type = "";
}
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
text = intent.getStringExtra(Intent.EXTRA_TEXT);
map.putString("value", text);
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : uris) {
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
map = Arguments.createMap();
text = "file://" + filePath;
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
} else {
type = "application/octet-stream";
}
map.putString("type", type);
items.pushMap(map);
}
}
}
return items;
}
private JSONObject buildPostObject(ReadableMap data) {
JSONObject json = new JSONObject();
try {
json.put("user_id", data.getString("currentUserId"));
json.put("channel_id", data.getString("channelId"));
json.put("message", data.getString("value"));
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
RequestBody body = RequestBody.create(JSON, postData.toString());
Request request = new Request.Builder()
.header("Authorization", "BEARER " + token)
.url(serverUrl + "/api/v4/posts")
.post(body)
.build();
Response response = client.newCall(request).execute();
}
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
try {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
for(int i = 0 ; i < files.size() ; i++) {
ReadableMap file = files.getMap(i);
String filePath = file.getString("fullPath").replaceFirst("file://", "");
File fileInfo = new File(filePath);
if (fileInfo.exists()) {
final MediaType MEDIA_TYPE = MediaType.parse(file.getString("mimeType"));
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(MEDIA_TYPE, fileInfo));
}
}
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
RequestBody body = builder.build();
Request request = new Request.Builder()
.header("Authorization", "BEARER " + token)
.url(serverUrl + "/api/v4/files")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseData = response.body().string();
JSONObject responseJson = new JSONObject(responseData);
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
JSONArray file_ids = new JSONArray();
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
file_ids.put(fileInfo.getString("id"));
}
postData.put("file_ids", file_ids);
post(serverUrl, token, postData);
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center_horizontal"
tools:context=".SplashScreenActivity">
<ImageView
android:id="@+id/imgLogo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</android.support.percent.PercentRelativeLayout>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 971 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 590 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

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