Compare commits

..

8 Commits

Author SHA1 Message Date
Elias Nahum
78aba1fcb6 Bump Android build number to 122 (#1883) 2018-07-04 10:50:39 -04:00
Elias Nahum
cba4908854 Bump iOS build number to 122 (#1882) 2018-07-04 10:41:47 -04:00
Harrison Healey
9169f6f694 Add setNativeProps to QuickTextInput (#1880) 2018-07-04 09:38:00 -04:00
Elias Nahum
bdc487ab20 Bump Android build number to 121 (#1875) 2018-07-03 19:44:54 -04:00
Elias Nahum
d7cc95c7f8 Bump iOS build number to 121 (#1874) 2018-07-03 19:39:18 -04:00
Elias Nahum
52ad889bfc Bump Android build number to 120 (#1869) 2018-07-03 19:20:07 -04:00
Elias Nahum
7be6911b2c Bump iOS build number to 120 (#1868) 2018-07-03 19:19:02 -04:00
Harrison Healey
509dfe5542 MM-11116 Re-added QuickTextInput and added another hack on top of it (#1871)
* MM-11116 Re-added updated QuickTextInput component to work around RN issue

* MM-11116 Work around setNativeProps not working for TextInputs

* Add isFocused method to QuickTextInput
2018-07-03 19:04:04 -04:00
904 changed files with 73877 additions and 52695 deletions

16
.babelrc Normal file
View File

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

View File

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

View File

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

View File

@@ -16,55 +16,33 @@
; Ignore polyfills
.*/Libraries/polyfills/.*
; Ignore metro
.*/node_modules/metro/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow-github/
[options]
emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
module.file_ext=.js
module.file_ext=.jsx
module.file_ext=.json
module.file_ext=.native.js
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.78.0
^0.56.0

12
.gitignore vendored
View File

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

View File

@@ -1,313 +1,5 @@
# Mattermost Mobile Apps Changelog
## 1.15.2 Release
- Release Date: January 16, 2019
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue where the status changes for other users did not always stay current in the mobile app.
- Fixed an issue where a post did not fail properly when the user attempted to send the post while there was no network access.
- Fixed an issue where date separators did not update when changing timezones.
- Fixed an issue where the Favorites section did not clear from a users's channel drawer.
- Removed an extra divider below "Edit Channel" of Direct Message Channel Info.
- Fixed an issue where a user was not returned to previously viewed channel after viewing and then closing an archived channel.
- Fixed an issue where a quick double tap on switch of Channel Info created and extra on/off state.
- Fixed an issue where iOS long press menu didn't have rounded corners.
## 1.15.1 Release
- Release Date: December 28, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Bug Fixes
- Fixed an issue preventing some users from logging in using OKTA.
## 1.15.0 Release
- Release Date: December 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
### Combatibility
- Mobile App v1.13+ is required for Mattermost Server v5.4+.
- Android operating system 7+ [is required by Google](https://android-developers.googleblog.com/2017/12/improving-app-security-and-performance.html).
### Highlights
- Added mention and reply mention highlighting.
- Added a sliding animation for the reaction list.
- Added support for pinned posts.
- Added support for jumbo emojis.
- Added support for interactive dialogs.
- Improved UI for the long press menu and emoji reaction viewer.
### Improvements
- Added the ability to include custom headers with requests for custom builds.
- Push Notifications that are grouped by channels are cleared once the channel is read.
- Improved auto-reconnect when unable to reach the server.
- Added support for changing the mobile client status to offline when the app loses connection.
- Added 'View Members' button to archived channels.
- Added support on iOS for keeping the postlist in place without scrolling when new content is available.
### Bug Fixes
- Fixed an issue where clicking on a file did not show downloading progress.
- Fixed an issue on Android where on fresh install the share extension would not properly show available channels.
- Fixed an issue where recently archived channels remained in in: autocomplete when they had been archived.
- Fixed an issue where text should render when no actual custom emoji matched the named emoji pattern.
- Fixed an issue on iOS where text got cut-off after replying to a message.
- Fixed an issue where search modifier for channels was showing Direct Messages without usernames.
- Fixed an issue where "Close Channel" did not work properly when viewing two archived channels in a row.
- Fixed an issue with "Critical Error" screen when trying to upload certain file types from "+" to the left of message input box.
## 1.14.0 Release
- Release Date: November 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported unless the user installs the CA certificate on their device
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue where the Android app did not allow establishing a network connection with any server that used a self-signed certificate that had the CA certificate user installed on the device.
- Removed "Copy Post" option on long-press message menu for posts without text.
- Fixed an issue where the "Search Results" header was not fully scrolled to top on search "from:username".
- Fixed an issue where channel names truncated at fewer characters than necessary.
- Fixed an issue where the same uploaded photo generated a different file size.
- Fixed an issue where the "(you)" was not displayed to the right of a user's name in the channel drawer when a user opened a Direct Message channel with themself.
- Fixed an issue where a dark theme set from webapp broke mobile display.
- Fixed an issue where channel drawer transition sometimes lagged.
- Fixed an issue where sending photos to Mattermost created large files.
- Fixed an issue where the apps showed "Select a Team" screen when opened.
- Fixed an issue where at-mention, emoji, and slash command autocompletes had a double top border.
- Fixed an issue where the drawer was unable to close when showing the team list.
- Fixed an issue where team sidebar showed + sign even without more teams to join.
## 1.13.1 Release
- Release Date: October 18, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Bug Fixes
- Fixed an issue preventing some users from authenticating using OKTA
## v1.13.0 Release
- Release Date: October 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
**Combatibility Note: Mobile App v1.13+ is required for Mattermost Server v5.4+**
### Highlights
#### View Emoji Reactions
- Hold down on any emoji reaction to see who reacted to the post.
#### Hashtags
- Added support for searching for hashtags in posts.
#### Dropdown menus
- Added support for dropdown menus in message attachments.
### Improvements
- Added support for iPhone XR, XS and XS Max.
- Added support for nicknames on user profile.
- On servers 5.4+, added support for searching in direct and group message channels using the "in:" modifier.
- Channel autocomplete now gets closed if multiple tildes are typed.
- Added a draft icon in sidebar and channel switcher for channels with unsent messages.
- Users are now redirected to the archived channel view (rather than to Town Square) when a channel is archived.
- When closing an archived channel, users are now returned to the previously viewed channel.
### Bug Fixes
- Refactored postlist to include Android Pie fixes and smoother scrolling.
- Fixed an issue where deactivated users were not marked as such in "Jump To" search.
- Fixed an issue where users got a permission error when trying to open a file from within the image preview screen.
- Fixed an issue where session expiry notifications were not being sent on Android.
- Fixed an issue where post attachments failed to upload.
- Fixed an issue where the "DM More..." list cut off user info.
- Fixed an issue where the user would briefly see a system message when loading a reply thread.
- Fixed an issue where the error message was incorrectly formatted if the login method was set to email/password and the user tried to log in with SAML.
- Fixed an issue on Android where the keyboard sometimes overlapped the bottom of the post textbox.
- Fixed an issue where there was no option to take video via "+" > "Take Photo or Video" on iOS.
## v1.12.0 Release
- Release Date: September 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Search Date Filters
- Search for messages before, on, or after a specified date.
### Improvements
- Added notification support for Android O and P.
### Bug Fixes
- Fixed an issue where Okta was not able to login in some deployments.
- Fixed an issue where messages in Direct Message channels did not show when clicking "Jump To".
- Fixed an issue where `Show More` on a post with a message attachment displayed a blank where content should have been.
- Prevent downloading of files when disallowed in the System Console.
- Fixed an issue where users could not click on attachment filenames to open them.
- Fixed an issue where email notification settings did not save from mobile.
- Fixed an issue where the share extension allowed users to select and attempt to share content to channels that had been archived.
- Fixed an issue where reacting to an existing emoji in an archived channel was allowed.
- Fixed an issue where archived channels sometimes remained in the drawer.
- Fixed an issue where deactivated users were not marked as such in Direct Message search.
## v1.11.0 Release
- Release Date: August 16, 2018
- Server Versions Supported: Server v4.10+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Searching Archived Channels
- Added ability to search for archived channels. Requires Mattermost server v5.2 or later.
#### Deep Linking
- Added the ability for custom builds to open Mattermost links directly in the app rather than the default mobile browser. Learn more in our [documentation](https://docs.mattermost.com/mobile/mobile-faq.html#how-do-i-configure-deep-linking)
### Improvements
- Added profile pop-up to combined system messages.
- Force re-entering SSO auth credentials after logout.
- Added consecutive posts by the same user.
- Added a loading indicator when user info is still loading in the left-hand side.
### Bug Fixes
- Fixed an issue where Android devices showed an incorrect timestamp.
- Fixed an issue on Android where the app did not get sent to the background when pressing the hardware back button in the channel screen.
- Fixed an issue with video playback when the filename had spaces.
- Fixed an issue where the app crashed when playing YouTube videos.
- Fixed an issue with session expiration notification.
- Fixed an issue with sharing files from Google Drive in Android Share Extension.
- Fixed an issue on Android where replying to a push notification sometimes went to the wrong channel.
- Fixed an issue where the previous server URL was present on the input textbox before changing the screen to Login.
- Fixed an issue where user menu was not translated correctly.
- Fixed an issue where some field lengths in Account Settings didn't match the desktop app.
- Fixed an issue where long URLs for embedded images in message attachments got cut off and didn't render.
- Fixed an issue where link preview images were not cropped properly.
- Fixed an issue where long usernames didn't wrap properly in the Account Settings menu.
- Fixed an issue where DMs would not open if users were using "Jump To".
- Fixed an issue where no message was displayed after removing a user from a channel with join/leave messages disabled.
## v1.10.0 Release
- Release Date: July 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Channel drawer performance
- Android devices will notice significant performance improvements when opening and closing the channel drawer.
#### Channel loading performance
- Improved channel loading performance as post are retrieved with every push notification
#### Announcement banner improvements
- Markdown now renders when announcement banners are expanded
- When enabled by the System Admin, users can now dismiss announcement banners until their next session
### Improvements
- Combined consecutive messages from the same user.
- Added experimental support for certificate-based authentication (CBA) for iOS to identify a user or a device before granting access to Mattermost. See [documentation](https://docs.mattermost.com/deployment/certificate-based-authentication.html) to learn more.
- Added support for the experimental automatic direct message replies feature.
- Added support for the experimental timezone feature.
- Changed post textbox to not be a connected component.
- Allow connecting to mattermost instances hosted at subpaths.
- Added support for starting YouTube videos at a given time.
- Added support for keeping messages if slash command fails.
### Bug Fixes
- Fixed an issue where the unread badge background was always white.
- Fixed an issue where a username repeated in system message if user was added to a channel more than once.
- Fixed an issue where Android Sharing from Microsoft apps failed.
- Fixed an issue where YouTube crashed the app if link did not have a time set.
- Fixed an issue where System Admins did not see all teams available to join on mobile.
- Fixed an issue where users were unable to share from Files app.
- Fixed an issue where viewing a non-existent permalink didn't show an error message.
- Fixed an issue where jumping to a channel search did not bold unread channels.
- Fixed an issue with being able to add own user to a Group Message channel.
- Fixed an issue with not being able to reply from a push notification on iOS.
- Fixed an issue where the app did not display Brazilian language.
## 1.9.3 Release
- Release Date: July 04, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed multiple issues causing app crashes
- Fixed an issue on iOS devices with typing non-english characters in the post input box
## 1.9.2 Release
- Release Date: June 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue where attached videos did not play for the poster
- Fixed an issue where "Jump to recent messages" from the permalink view did not direct the user to the bottom of the channel
- Fixed an issue where post comments did not identify which parent post they belonged to
- Fixed multiple issues with typing non-english characters in the post input box
- Fixed multiple issues causing random app crashes
- Fixed an issue where files from the Android Files app failed to upload
- Fixed an issue where the iOS share extension crashed when switching the team or channel
- Fixed an issue where files from the Microsoft app failed to upload
- Fixed an issue on Android devices where sharing files changed the file extension of the attachment
## 1.9.1 Release
- Release Date: June 23, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with typing lag on Android devices
- Fixed an issue causing users to be logged out after upgrading to v1.9.0
- Fixed an issue where the ``in:`` and ``from:`` modifiers were not being added to the search field
## v1.9.0 Release
- Release Date: June 16, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Improved first load time on Android
- Significantly decreased first load time on Android devices from cold start.
#### iOS Files app support
- Added support for attaching files from the iOS Files app from within Mattermost.
#### Improved styling of push notification
- Improved the layout of message content, channel name and sender name in push notifications.
### Improvements
- Combined join/leave system messages.
- Added splash screen and channel loader improvements.
- Removed the desktop notification duration setting.
- Added cache team icon and set background to always be white if using a PNG file.
- Added whitelabel for icons and splash screen.
### Bug Fixes
- Fixed an issue where other user's display name did not render in combined system messages after joining the channel.
- Fixed an issue where posts incorrectly had "Commented on Someone's message" above them.
- Fixed an issue where deleting a post or its parent in permalink view left permalink view blank.
- Fixed an issue where "User is typing" message cut was off.
- Fixed an issue where `More New Messages Above` appeared at the top of new channel on joining.
- Fixed an issue where a user was not directed to Town Square when leaving a channel.
- Fixed an issue where long post were not collapsed on Android.
- Fixed an issue where a user's name was initially shown as "someone" when opening a direct message with the user.
- Fixed an issue where an error was received when trying to change the team or channel from the share extension.
- Fixed an issue where switching to a newly created channel from a push notification redirected a user to Town Square.
- Fixed an issue where a public channel made private did not disappear automatically from clients not part of the channel.
## v1.8.0 Release
- Release Date: April 27, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported

View File

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

View File

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

143
Makefile
View File

@@ -1,18 +1,16 @@
.PHONY: pre-run pre-build clean
.PHONY: pre-run clean
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build build-ios build-android unsigned-ios unsigned-android ios-sim-x86_64
.PHONY: build-pr can-build-pr prepare-pr
.PHONY: build-ios build-android unsigned-ios unsigned-android
.PHONY: test help
POD := $(shell which pod 2> /dev/null)
OS := $(shell sh -c 'uname -s 2>/dev/null')
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
MM_UTILITIES_DIR = ../mattermost-utilities
node_modules: package.json
.npminstall: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
@@ -21,14 +19,7 @@ node_modules: package.json
@echo Getting Javascript dependencies
@npm install
npm-ci: package.json
@if ! [ $(shell which npm 2> /dev/null) ]; then \
echo "npm is not installed https://npmjs.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@npm ci
@touch $@
.podinstall:
ifeq ($(OS), Darwin)
@@ -52,11 +43,9 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | node_modules .podinstall dist/assets ## Installs dependencies and assets
pre-run: | .npminstall .podinstall dist/assets ## Installs dependencies and assets
pre-build: | npm-ci .podinstall dist/assets ## Install dependencies and assets before building
check-style: node_modules ## Runs eslint
check-style: .npminstall ## Runs eslint
@echo Checking for style guide compliance
@npm run check
@@ -64,6 +53,7 @@ clean: ## Cleans dependencies, previous builds and temp files
@echo Cleaning started
@rm -rf node_modules
@rm -f .npminstall
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@@ -73,6 +63,11 @@ clean: ## Cleans dependencies, previous builds and temp files
@echo Cleanup finished
post-install:
@./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
@# Must remove the .babelrc for 0.42.0 to work correctly
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
@cp ./native_modules/ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
@# Need to copy custom RNDocumentPicker.m that implements direct access to the document picker in iOS
@cp ./native_modules/RNDocumentPicker.m node_modules/react-native-document-picker/ios/RNDocumentPicker/RNDocumentPicker.m
@@ -89,12 +84,24 @@ post-install:
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@cd ./node_modules/mattermost-redux && npm run build
start: | pre-run ## Starts the React Native packager server
$(call start_packager)
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start; \
else \
echo React Native packager server already running; \
fi
stop: ## Stops the React Native packager server
$(call stop_packager)
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
check-device-ios:
@if ! [ $(shell which xcodebuild) ]; then \
@@ -132,7 +139,7 @@ prepare-android-build:
run: run-ios ## alias for run-ios
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo Running iOS app in development; \
if [ ! -z "${SIMULATOR}" ]; then \
@@ -151,7 +158,7 @@ run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
fi
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
@@ -169,93 +176,51 @@ run-android: | check-device-android pre-run prepare-android-build ## Runs the ap
fi; \
fi
build: | stop pre-build check-style ## Builds the app for Android & iOS
$(call start_packager)
@echo "Building App"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build
$(call stop_packager)
build-ios: | stop pre-build check-style ## Builds the iOS app
$(call start_packager)
build-ios: | pre-run check-style ## Creates an iOS build
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
$(call stop_packager)
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
build-android: | stop pre-build check-style prepare-android-build ## Build the Android app
$(call start_packager)
build-android: | pre-run check-style prepare-android-build ## Creates an Android build
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
$(call stop_packager)
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
unsigned-ios: stop pre-build check-style ## Build an unsigned version of the iOS app
$(call start_packager)
unsigned-ios: pre-run check-style
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building unsigned iOS app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@rm -rf build-ios/
$(call stop_packager)
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
ios-sim-x86_64: stop pre-build check-style ## Build an unsigned x86_64 version of the iOS app for iPhone simulator
$(call start_packager)
@echo "Building unsigned x86_64 iOS app for iPhone simulator"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -arch x86_64 -sdk iphonesimulator -configuration Release -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ ENABLE_BITCODE=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ENABLE_BITCODE=NO
@cd build-ios/Build/Products/Release-iphonesimulator/ && zip -r Mattermost-simulator-x86_64.app.zip Mattermost.app/
@mv build-ios/Build/Products/Release-iphonesimulator/Mattermost-simulator-x86_64.app.zip .
@rm -rf build-ios/
$(call stop_packager)
unsigned-android: stop pre-build check-style prepare-android-build ## Build an unsigned version of the Android app
$(call start_packager)
unsigned-android: pre-run check-style prepare-android-build
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
fi
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/unsigned/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
$(call stop_packager)
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
test: | pre-run check-style ## Runs tests
@npm test
build-pr: | can-build-pr stop pre-build check-style ## Build a PR from the mattermost-mobile repo
$(call start_packager)
@echo "Building App from PR ${PR_ID}"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane build_pr pr:PR-${PR_ID}
$(call stop_packager)
can-build-pr:
@if [ -z ${PR_ID} ]; then \
echo a PR number needs to be specified; \
exit 1; \
fi
i18n-extract: ## Extract strings for translation from the source code
@[[ -d $(MM_UTILITIES_DIR) ]] || echo "You must clone github.com/mattermost/mattermost-utilities repo in .. to use this command"
@[[ -d $(MM_UTILITIES_DIR) ]] && cd $(MM_UTILITIES_DIR) && npm install && npm run babel && node mmjstool/build/index.js i18n extract-mobile
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
define start_packager
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
npm start & echo; \
else \
echo React Native packager server already running; \
fi
endef
define stop_packager
@echo Stopping React Native packager server
@if [ $(shell ps -ef | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -ef | grep -i "cli.js start" | grep -iv grep | awk '{print $$2}' | xargs kill -9; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
endef

3687
NOTICE.txt

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,16 @@
# Mattermost Mobile
- **Supported Server versions:** 4.10+
- **Supported iOS versions:** 10.3+
- **Supported Android versions:** 7.0+
**Supported Server Versions:** 4.0+
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://about.mattermost.com/default-enterprise-app-store).
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
# How to Contribute
### Testing
@@ -21,7 +20,7 @@ To help with testing app updates before they're released, you can:
1. Sign up to be a beta tester
- [Android](https://play.google.com/apps/testing/com.mattermost.rnbeta)
- [iOS](https://mattermost-fastlane.herokuapp.com/)
2. Install the `Mattermost Beta` app. New updates in the Beta app are released periodically. You will receive a notification when the new updates are available.
2. Install the `Mattermost Beta` app. New updates in the Beta app are released each Monday. You will receive a notification when the new updates are available.
3. File any bugs you find by filing a [GitHub issue](https://github.com/mattermost/mattermost-mobile/issues) with:
- Device information
- Repro steps
@@ -32,9 +31,9 @@ To help with testing app updates before they're released, you can:
### Contribute Code
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-server/issues) for issues marked as [Help Wanted]
1. Look in [GitHub issues](https://github.com/mattermost/mattermost-mobile/issues) for issues marked as [Help Wanted]
2. Comment to let people know 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

View File

@@ -45,13 +45,13 @@ android_library(
android_build_config(
name = "build_config",
package = "com.mattermost.rnbeta",
package = "com.mattermost-mobile",
)
android_resource(
name = "res",
package = "com.mattermost.rnbeta",
res = "src/main/res",
name = "res",
res = "src/main/res",
package = "com.mattermost.rnbeta",
)
android_binary(

View File

@@ -74,8 +74,8 @@ import com.android.build.OutputFile
project.ext.react = [
entryFile: "index.js",
bundleCommand: "ram-bundle",
bundleConfig: "packager-config.js"
bundleCommand: "unbundle",
bundleConfig: "packager/config.js"
]
apply from: "../../node_modules/react-native/react.gradle"
@@ -106,21 +106,15 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
packagingOptions {
pickFirst 'lib/x86_64/libjsc.so'
pickFirst 'lib/arm64-v8a/libjsc.so'
}
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 182
versionName "1.17.0"
multiDexEnabled = true
minSdkVersion 21
targetSdkVersion 23
versionCode 122
versionName "1.9.3"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -157,7 +151,6 @@ android {
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
matchingFallbacks = ['debug', 'release']
}
}
// applicationVariants are e.g. debug, release
@@ -185,60 +178,46 @@ configurations.all {
resolutionStrategy {
eachDependency { DependencyResolveDetails details ->
if (details.requested.name == 'android-jsc') {
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r236355'
}
if (details.requested.name == 'play-services-base') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-tasks') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-stats') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
details.useTarget group: details.requested.group, name: 'android-jsc-intl', version: 'r216113'
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:percent:28.0.0'
implementation "com.google.firebase:firebase-messaging:17.3.0"
implementation "com.facebook.react:react-native:+" // From node_modules
implementation project(':react-native-document-picker')
implementation project(':react-native-keychain')
implementation project(':react-native-doc-viewer')
implementation project(':react-native-video')
implementation project(':react-native-navigation')
implementation project(':react-native-image-picker')
implementation project(':react-native-bottom-sheet')
implementation project(':react-native-device-info')
implementation project(':reactnativenotifications')
implementation project(':react-native-cookies')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-vector-icons')
implementation project(':react-native-svg')
implementation project(':react-native-local-auth')
implementation project(':jail-monkey')
implementation project(':react-native-youtube')
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':rn-fetch-blob')
implementation project(':react-native-recyclerview-list')
implementation project(':react-native-webview')
implementation project(':react-native-gesture-handler')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile 'com.android.support:percent:25.3.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-document-picker')
compile project(':react-native-keychain')
compile project(':react-native-doc-viewer')
compile project(':react-native-video')
compile project(':react-native-navigation')
compile project(':react-native-image-picker')
compile project(':react-native-bottom-sheet')
compile ('com.google.android.gms:play-services-gcm:9.4.0') {
force = true;
}
compile project(':react-native-device-info')
compile project(':reactnativenotifications')
compile project(':react-native-cookies')
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-local-auth')
compile project(':jail-monkey')
compile project(':react-native-youtube')
compile project(':react-native-sentry')
compile project(':react-native-exception-handler')
compile project(':react-native-fetch-blob')
// For animated GIF support
implementation 'com.facebook.fresco:fresco:1.10.0'
implementation 'com.facebook.fresco:animated-gif:1.10.0'
compile 'com.facebook.fresco:animated-base-support:1.3.0'
// For WebP support, including animated WebP
implementation 'com.facebook.fresco:animated-webp:1.10.0'
implementation 'com.facebook.fresco:webpsupport:1.10.0'
compile 'com.facebook.fresco:animated-gif:1.3.0'
compile 'com.facebook.fresco:animated-webp:1.3.0'
compile 'com.facebook.fresco:webpsupport:1.3.0'
}
// Run this once to be able to run the application with BUCK
@@ -247,5 +226,3 @@ task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply plugin: 'com.google.gms.google-services'

View File

@@ -15,3 +15,56 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**

View File

@@ -1,32 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mattermost.rnbeta">
package="com.mattermost.rnbeta"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission
android:name="${applicationId}.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="com.google.android.c2dm.permission.SEND" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:networkSecurityConfig="@xml/network_security_config"
>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\"/>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize">
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@@ -1,7 +1,6 @@
package com.mattermost.react_native_interface;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
/**
* ResolvePromise: Helper class that abstracts boilerplate
@@ -17,41 +16,16 @@ public class ResolvePromise implements Promise {
}
@Override
public void reject(String code, WritableMap map) {
}
@Override
public void reject(String code, Throwable e) {
}
@Override
public void reject(Throwable e, WritableMap map) {
}
@Override
public void reject(String code, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e, WritableMap map) {
}
@Override
public void reject(String code, String message, Throwable e) {
}
@Override
public void reject(String code, String message, WritableMap map) {
}
@Override
public void reject(String message) {

View File

@@ -1,7 +1,6 @@
package com.mattermost.rnbeta;
import android.app.PendingIntent;
import android.app.NotificationChannel;
import android.content.Intent;
import android.content.Context;
import android.content.res.Resources;
@@ -47,46 +46,31 @@ public class CustomPushNotification extends PushNotification {
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
private static int badgeCount = 0;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
this.context = context;
}
public static void clearNotification(Context mContext, int notificationId, String channelId) {
public static void clearNotification(int notificationId, String channelId) {
if (notificationId != -1) {
Object objCount = channelIdToNotificationCount.get(channelId);
Integer count = -1;
if (objCount != null) {
count = (Integer)objCount;
}
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (context != null) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
if (count != -1) {
int total = CustomPushNotification.badgeCount - count;
int badgeCount = total < 0 ? 0 : total;
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), badgeCount);
CustomPushNotification.badgeCount = badgeCount;
}
}
}
}
public static void clearAllNotifications(Context mContext) {
channelIdToNotificationCount.clear();
channelIdToNotification.clear();
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), 0);
public static void clearNotification(Context mContext, int notificationId, String channelId) {
if (notificationId != -1) {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
}
@@ -134,6 +118,7 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
digestNotification();
clearAllNotifications();
}
@Override
@@ -155,19 +140,8 @@ public class CustomPushNotification extends PushNotification {
String packageName = mContext.getPackageName();
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
String CHANNEL_ID = "channel_01";
String CHANNEL_NAME = "Mattermost notifications";
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(channel);
notification.setChannelId(CHANNEL_ID);
}
Bundle bundle = mNotificationProps.asBundle();
String version = bundle.getString("version");
@@ -193,11 +167,9 @@ public class CustomPushNotification extends PushNotification {
String largeIcon = bundle.getString("largeIcon");
Bundle b = bundle.getBundle("userInfo");
if (b == null) {
b = new Bundle();
if (b != null) {
notification.addExtras(b);
}
b.putString("channel_id", channelId);
notification.addExtras(b);
int smallIconResId;
int largeIconResId;
@@ -223,8 +195,7 @@ public class CustomPushNotification extends PushNotification {
}
if (numberString != null) {
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
int numMessages = getMessageCountInChannel(channelId);
@@ -300,13 +271,7 @@ public class CustomPushNotification extends PushNotification {
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
replyPendingIntent = PendingIntent.getForegroundService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
} else {
replyPendingIntent = PendingIntent.getService(mContext, notificationId, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
@@ -371,11 +336,15 @@ public class CustomPushNotification extends PushNotification {
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");
final String numberString = data.getString("badge");
CustomPushNotification.badgeCount = Integer.parseInt(numberString);
CustomPushNotification.clearNotification(mContext.getApplicationContext(), notificationId, channelId);
String numberString = data.getString("badge");
if (numberString != null) {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), CustomPushNotification.badgeCount);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}

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,15 +1,10 @@
package com.mattermost.rnbeta;
import com.mattermost.share.SharePackage;
import com.mattermost.share.RealPathUtil;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import com.reactnativedocumentpicker.ReactNativeDocumentPicker;
import com.oblador.keychain.KeychainPackage;
@@ -22,9 +17,6 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
@@ -39,15 +31,14 @@ import com.reactnativenavigation.NavigationApplication;
import com.wix.reactnativenotifications.RNNotificationsPackage;
import com.wix.reactnativenotifications.core.notification.INotificationsApplication;
import com.wix.reactnativenotifications.core.notification.IPushNotification;
import com.wix.reactnativenotifications.core.notificationdrawer.IPushNotificationsDrawer;
import com.wix.reactnativenotifications.core.notificationdrawer.INotificationsDrawerApplication;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import android.util.Log;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
public Boolean sharedExtensionIsOpened = false;
public Boolean replyFromPushNotification = false;
@@ -75,7 +66,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new JailMonkeyPackage(),
new RNFetchBlobPackage(),
new MattermostPackage(this),
new RNSentryPackage(),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
@@ -83,10 +74,7 @@ public class MainApplication extends NavigationApplication implements INotificat
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this),
new RNRecyclerviewListPackage(),
new RNCWebViewPackage(),
new RNGestureHandlerPackage()
new InitializationPackage(this)
);
}
@@ -99,12 +87,6 @@ public class MainApplication extends NavigationApplication implements INotificat
public void onCreate() {
super.onCreate();
instance = this;
// Delete any previous temp files created by the app
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
RealPathUtil.deleteTempFiles(tempFolder);
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
// Create an object of the custom facade impl
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
// Attach it to react-native-navigation
@@ -114,7 +96,7 @@ public class MainApplication extends NavigationApplication implements INotificat
}
@Override
public boolean clearHostOnActivityDestroy(Activity activity) {
public boolean clearHostOnActivityDestroy() {
// This solves the issue where the splash screen does not go away
// after the app is killed by the OS cause of memory or a long time in the background
return false;
@@ -130,9 +112,4 @@ public class MainApplication extends NavigationApplication implements INotificat
new JsIOHelper()
);
}
@Override
public IPushNotificationsDrawer getPushNotificationsDrawer(Context context, AppLaunchHelper defaultAppLaunchHelper) {
return new CustomPushNotificationDrawer(context, defaultAppLaunchHelper);
}
}

View File

@@ -1,15 +1,12 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.database.Cursor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.os.Bundle;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
@@ -108,29 +105,4 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
public void setShouldBlink(boolean blink) {
mNotificationPreference.setShouldBlink(blink);
}
@ReactMethod
public void getDeliveredNotifications(final Promise promise) {
Context context = mApplication.getApplicationContext();
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
StatusBarNotification[] statusBarNotifications = notificationManager.getActiveNotifications();
WritableArray result = Arguments.createArray();
for (StatusBarNotification sbn:statusBarNotifications) {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
int identifier = sbn.getId();
String channelId = bundle.getString("channel_id");
map.putInt("identifier", identifier);
map.putString("channel_id", channelId);
result.pushMap(map);
}
promise.resolve(result);
}
@ReactMethod
public void removeDeliveredNotifications(int identifier, String channelId) {
Context context = mApplication.getApplicationContext();
CustomPushNotification.clearNotification(context, identifier, channelId);
}
}

View File

@@ -1,13 +1,9 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -22,28 +18,6 @@ import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
public void onCreate() {
super.onCreate();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Context mContext = this.getApplicationContext();
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
int smallIconResId = res.getIdentifier("ic_notification", "mipmap", packageName);
String CHANNEL_ID = "Reply job";
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, NotificationManager.IMPORTANCE_LOW);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
Notification notification =
new Notification.Builder(mContext, CHANNEL_ID)
.setContentTitle("Replying to message")
.setContentText(packageName)
.setSmallIcon(smallIconResId)
.build();
startForeground(1, notification);
}
}
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();

View File

@@ -10,7 +10,6 @@ import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.content.res.Configuration;
@@ -70,13 +69,9 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig != null && managedConfig.size() > 0 && activity != null) {
if (managedConfig!= null && managedConfig.size() > 0 && activity != null) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
if (activity != null) {
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
}
@Override

View File

@@ -6,7 +6,6 @@ import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.content.ContentUris;
import android.content.ContentResolver;
import android.os.Environment;
@@ -45,7 +44,9 @@ public class RealPathUtil {
return id.replaceFirst("raw:", "");
}
try {
return getPathFromSavingTempFile(context, uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} catch (NumberFormatException e) {
Log.e("ReactNative", "DownloadsProvider unexpected uri " + uri.toString());
return null;
@@ -94,33 +95,15 @@ public class RealPathUtil {
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
File tmpFile;
String fileName = null;
// Try and get the filename from the Uri
try {
Cursor returnCursor =
context.getContentResolver().query(uri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
fileName = returnCursor.getString(nameIndex);
} catch (Exception e) {
// just continue to get the filename with the last segment of the path
}
try {
if (fileName == null) {
fileName = uri.getLastPathSegment().toString().trim();
}
String fileName = uri.getLastPathSegment();
File cacheDir = new File(context.getCacheDir(), "mmShare");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
String mimeType = getMimeType(uri.getPath());
tmpFile = new File(cacheDir, fileName);
tmpFile.createNewFile();
tmpFile = File.createTempFile("tmp", fileName, cacheDir);
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
@@ -205,12 +188,8 @@ public class RealPathUtil {
}
public static String getMimeTypeFromUri(final Context context, final Uri uri) {
try {
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
} catch (Exception e) {
return "application/octet-stream";
}
ContentResolver cR = context.getContentResolver();
return cR.getType(uri);
}
public static void deleteTempFiles(final File dir) {

View File

@@ -77,7 +77,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
this.clear();
getCurrentActivity().finish();
if (data != null && data.hasKey("url")) {
if (data != null) {
ReadableArray files = data.getArray("files");
String serverUrl = data.getString("url");
String token = data.getString("token");
@@ -145,19 +145,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : uris) {
@@ -167,15 +165,12 @@ public class ShareModule extends ReactContextBaseJavaModule {
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
} else {
type = "application/octet-stream";
if (type.equals("image/*")) {
type = "image/jpeg";
} else if (type.equals("video/*")) {
type = "video/mp4";
}
map.putString("type", type);
items.pushMap(map);
}

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 459 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 585 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 590 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 468 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 608 KiB

View File

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

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<!-- Trust preinstalled CAs -->
<certificates src="system" />
<!-- Additionally trust user added CAs -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -1,42 +1,20 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 24
compileSdkVersion = 28
targetSdkVersion = 26
supportLibVersion = "28.0.0"
}
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.android.tools.build:gradle:2.2.+'
classpath 'com.google.gms:google-services:3.1.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
subprojects {
afterEvaluate {
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}
}
}
allprojects {
repositories {
google()
mavenLocal()
jcenter()
maven {
@@ -45,7 +23,7 @@ allprojects {
}
maven {
// Local Maven repo containing AARs with JSC library built for Android
url "$rootDir/../node_modules/jsc-android/dist"
url "$rootDir/../node_modules/jsc-android/android"
}
}
}

View File

@@ -11,12 +11,10 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048M
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
#android.enableAapt2=false
#android.useDeprecatedNdk=true
android.useDeprecatedNdk=true

Binary file not shown.

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

110
android/gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/usr/bin/env bash
##############################################################################
##
@@ -6,6 +6,47 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -20,49 +61,9 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -89,7 +90,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -113,7 +114,6 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,19 +154,11 @@ if $cygwin ; then
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
APP_ARGS=$(save "$@")
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

14
android/gradlew.bat vendored
View File

@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,9 +46,10 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -59,6 +60,11 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@@ -1,6 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':react-native-document-picker'
project(':react-native-document-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-document-picker/android')
include ':react-native-keychain'
@@ -15,8 +13,8 @@ include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
include ':rn-fetch-blob'
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/rn-fetch-blob/android')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
include ':jail-monkey'
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
include ':react-native-local-auth'
@@ -41,7 +39,3 @@ include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-recyclerview-list'
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')

View File

@@ -1,19 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {networkStatusChangedAction} from 'redux-offline';
import {Client4} from 'mattermost-redux/client';
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {DeviceTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch) => {
Client4.setOnline(isOnline);
dispatch(networkStatusChangedAction(isOnline));
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
});
}, getState);
};
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {updateMe} from 'mattermost-redux/actions/users';
import {Preferences} from 'mattermost-redux/constants';
import {savePreferences} from 'mattermost-redux/actions/preferences';
export function handleUpdateUserNotifyProps(notifyProps) {
return async (dispatch, getState) => {
const state = getState();
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, user_id: userId, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id: userId,
category: Preferences.CATEGORY_NOTIFICATIONS,
name: Preferences.EMAIL_INTERVAL,
value: interval,
}];
savePreferences(currentUserId, emailInterval)(dispatch, getState);
}
const props = {...otherProps, email};
try {
await updateMe({notify_props: props})(dispatch, getState);
} catch (error) {
// do nothing
}
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
@@ -9,8 +9,8 @@ import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
markChannelAsRead,
leaveChannel as serviceLeaveChannel, markChannelAsViewed,
selectChannel,
leaveChannel as serviceLeaveChannel,
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
@@ -18,8 +18,8 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getChannel, getCurrentChannelId, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
import {
getChannelByName,
@@ -160,7 +160,6 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
const postsIds = postsInChannel[channelId];
const actions = [];
const time = Date.now();
@@ -171,15 +170,7 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
if (received) {
const count = received.order.length;
loadMorePostsVisible = count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
actions.push({
type: ViewTypes.SET_INITIAL_POST_COUNT,
data: {
channelId,
count,
},
});
loadMorePostsVisible = received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
}
} else {
const {lastConnectAt} = state.device.websocket;
@@ -199,34 +190,25 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
if (received) {
const count = received.order.length;
loadMorePostsVisible = postsIds.length + count >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
actions.push({
type: ViewTypes.SET_INITIAL_POST_COUNT,
data: {
channelId,
count: postsIds.length + count,
},
});
loadMorePostsVisible = postsIds.length + received.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
}
}
if (received) {
actions.push({
dispatch({
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
channelId,
time,
});
}
actions.push(setLoadMorePostsVisible(loadMorePostsVisible));
dispatch(batchActions(actions));
dispatch(setLoadMorePostsVisible(loadMorePostsVisible));
};
}
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const {data} = await dispatch(action); // eslint-disable-line no-await-in-loop
const {data} = await action(dispatch, getState);
if (data) {
dispatch(setChannelRetryFailed(false));
@@ -243,8 +225,8 @@ export function loadFilesForPostIfNecessary(postId) {
const {files} = getState().entities;
const fileIdsForPost = files.fileIdsByPostId[postId];
if (!fileIdsForPost?.length) {
await dispatch(getFilesForPost(postId));
if (!fileIdsForPost) {
await getFilesForPost(postId)(dispatch, getState);
}
};
}
@@ -277,43 +259,10 @@ export function selectInitialChannel(teamId) {
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
if (
myMembers[lastChannelId] &&
lastChannel &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
dispatch(handleSelectChannel(lastChannelId));
return;
}
dispatch(selectDefaultChannel(teamId));
};
}
export function selectPenultimateChannel(teamId) {
return (dispatch, getState) => {
const state = getState();
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length > 1 ? lastChannelForTeam[1] : '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
if (
myMembers[lastChannelId] &&
lastChannel &&
lastChannel.delete_at === 0 &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)
) {
dispatch(setChannelLoading(true));
dispatch(handleSelectChannel(lastChannelId));
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
}
@@ -323,7 +272,7 @@ export function selectPenultimateChannel(teamId) {
export function selectDefaultChannel(teamId) {
return (dispatch, getState) => {
const {channels} = getState().entities.channels;
const channels = getState().entities.channels.channels;
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
@@ -339,30 +288,23 @@ export function selectDefaultChannel(teamId) {
}
if (channelId) {
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
dispatch(markChannelAsRead(channelId));
}
};
}
export function handleSelectChannel(channelId, fromPushNotification = false) {
export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const state = getState();
const channel = getChannel(state, channelId);
const currentTeamId = getCurrentTeamId(state);
const currentChannelId = getCurrentChannelId(state);
const sameChannel = channelId === currentChannelId;
const member = getMyChannelMember(state, channelId);
const {currentTeamId} = getState().entities.teams;
dispatch(setLoadMorePostsVisible(true));
// If the app is open from push notification, we already fetched the posts.
if (!fromPushNotification) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
selectChannel(channelId),
setChannelDisplayName(channel.display_name),
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
@@ -373,19 +315,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
teamId: currentTeamId,
channelId,
},
{
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
member,
},
]));
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
}
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
@@ -410,13 +340,6 @@ export function insertToDraft(value) {
};
}
export function markChannelViewedAndRead(channelId, previousChannelId, markOnServer = true) {
return (dispatch) => {
dispatch(markChannelAsRead(channelId, previousChannelId, markOnServer));
dispatch(markChannelAsViewed(channelId, previousChannelId));
};
}
export function toggleDMChannel(otherUserId, visible, channelId) {
return async (dispatch, getState) => {
const state = getState();
@@ -434,7 +357,7 @@ export function toggleDMChannel(otherUserId, visible, channelId) {
value: Date.now().toString(),
}];
dispatch(savePreferences(currentUserId, dm));
savePreferences(currentUserId, dm)(dispatch, getState);
};
}
@@ -450,7 +373,7 @@ export function toggleGMChannel(channelId, visible) {
value: visible,
}];
dispatch(savePreferences(currentUserId, gm));
savePreferences(currentUserId, gm)(dispatch, getState);
};
}
@@ -459,9 +382,9 @@ export function closeDMChannel(channel) {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
dispatch(toggleDMChannel(channel.teammate_id, 'false'));
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
dispatch(selectInitialChannel(state.entities.teams.currentTeamId));
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
}
@@ -505,7 +428,7 @@ export function leaveChannel(channel, reset = false) {
await dispatch(selectDefaultChannel(currentTeamId));
}
await dispatch(serviceLeaveChannel(channel.id));
await serviceLeaveChannel(channel.id)(dispatch, getState);
};
}
@@ -545,12 +468,13 @@ export function increasePostVisibility(channelId, focusedPostId) {
const currentPostVisibility = postVisibility[channelId] || 0;
if (loadingPosts[channelId]) {
return true;
return false;
}
// Check if we already have the posts that we want to show
if (!focusedPostId) {
const loadedPostCount = state.views.channel.postCountInChannel[channelId] || 0;
const postsInChannel = state.entities.posts.postsInChannel[channelId] || [];
const loadedPostCount = postsInChannel.length;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (loadedPostCount >= desiredPostVisibility) {
@@ -575,9 +499,9 @@ export function increasePostVisibility(channelId, focusedPostId) {
let result;
if (focusedPostId) {
result = await retryGetPostsAction(getPostsBefore(channelId, focusedPostId, page, pageSize), dispatch, getState);
result = await getPostsBefore(channelId, focusedPostId, page, pageSize)(dispatch, getState);
} else {
result = await retryGetPostsAction(getPosts(channelId, page, pageSize), dispatch, getState);
result = await getPosts(channelId, page, pageSize)(dispatch, getState);
}
const actions = [{
@@ -586,28 +510,18 @@ export function increasePostVisibility(channelId, focusedPostId) {
channelId,
}];
let hasMorePost = false;
if (result) {
const count = result.order.length;
hasMorePost = count >= pageSize;
actions.push({
type: ViewTypes.INCREASE_POST_COUNT,
data: {
channelId,
count,
},
});
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results
actions.push(doIncreasePostVisibility(channelId));
actions.push(setLoadMorePostsVisible(hasMorePost));
actions.push(setLoadMorePostsVisible(posts.order.length >= pageSize));
}
dispatch(batchActions(actions));
return hasMorePost;
return Boolean(posts);
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addChannelMember} from 'mattermost-redux/actions/channels';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {removeChannelMember} from 'mattermost-redux/actions/channels';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {IntegrationTypes} from 'mattermost-redux/action_types';
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
@@ -28,12 +27,6 @@ export function executeCommand(message, channelId, rootId) {
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
const {data, error} = await dispatch(executeCommandService(msg, args));
if (data.trigger_id) {
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}
return {data, error};
return await executeCommandService(msg, args)(dispatch, getState);
};
}
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {handleSelectChannel, setChannelDisplayName} from './channel';
import {createChannel} from 'mattermost-redux/actions/channels';
@@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
type,
};
const {data} = await dispatch(createChannel(channel, currentUserId));
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
if (data && data.id) {
dispatch(setChannelDisplayName(displayName));
dispatch(handleSelectChannel(data.id));
handleSelectChannel(data.id)(dispatch, getState);
}
};
}

View File

@@ -1,9 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {updateMe, setDefaultProfileImage} from 'mattermost-redux/actions/users';
import {ViewTypes} from 'app/constants';
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
import {buildFileUploadData} from 'app/utils/file';
export function updateUser(user, success, error) {
return async (dispatch, getState) => {
@@ -18,22 +17,9 @@ export function updateUser(user, success, error) {
};
}
export function setProfileImageUri(imageUri = '') {
return {
type: ViewTypes.SET_PROFILE_IMAGE_URI,
imageUri,
export function handleUploadProfileImage(image, userId) {
return async (dispatch, getState) => {
const imageData = buildFileUploadData(image);
return await uploadProfileImage(userId, imageData)(dispatch, getState);
};
}
export function removeProfileImage(user) {
return async (dispatch) => {
const result = await dispatch(setDefaultProfileImage(user));
return result;
};
}
export default {
updateUser,
setProfileImageUri,
removeProfileImage,
};

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {FileTypes} from 'mattermost-redux/action_types';

View File

@@ -1,18 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {getSessions} from 'mattermost-redux/actions/users';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {ViewTypes} from 'app/constants';
import {app} from 'app/mattermost';
import PushNotifications from 'app/push_notifications';
import {getDeviceTimezone, isTimezoneEnabled} from 'app/utils/timezone';
export function handleLoginIdChanged(loginId) {
return async (dispatch, getState) => {
@@ -35,8 +30,7 @@ export function handlePasswordChanged(password) {
export function handleSuccessfulLogin() {
return async (dispatch, getState) => {
const state = getState();
const config = getConfig(state);
const license = getLicense(state);
const {config, license} = state.entities.general;
const token = Client4.getToken();
const url = Client4.getUrl();
const deviceToken = state.entities.general.deviceToken;
@@ -44,22 +38,17 @@ export function handleSuccessfulLogin() {
app.setAppCredentials(deviceToken, currentUserId, token, url);
const enableTimezone = isTimezoneEnabled(state);
if (enableTimezone) {
dispatch(autoUpdateTimezone(getDeviceTimezone()));
}
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url,
token,
},
});
}, getState);
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
@@ -68,46 +57,22 @@ export function handleSuccessfulLogin() {
};
}
export function scheduleExpiredNotification(intl) {
return (dispatch, getState) => {
export function getSession() {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
const {deviceToken} = state.entities.general;
const message = intl.formatMessage({
id: 'mobile.session_expired',
defaultMessage: 'Session Expired: Please log in to continue receiving notifications.',
});
const {credentials} = state.entities.general;
const token = credentials && credentials.token;
// Once the user logs in we are going to wait for 10 seconds
// before retrieving the session that belongs to this device
// to ensure that we get the actual session without issues
// then we can schedule the local notification for the session expired
setTimeout(async () => {
let sessions;
try {
sessions = await dispatch(getSessions(currentUserId));
} catch (e) {
console.warn('Failed to get current session', e); // eslint-disable-line no-console
return;
if (currentUserId && token) {
const session = await Client4.getSessions(currentUserId, token);
if (Array.isArray(session) && session[0]) {
const s = session[0];
return s.expires_at;
}
}
if (!Array.isArray(sessions.data)) {
return;
}
const session = sessions.data.find((s) => s.device_id === deviceToken);
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
if (expiresAt) {
PushNotifications.localNotificationSchedule({
date: new Date(expiresAt),
message,
userInfo: {
localNotification: true,
},
});
}
}, 10000);
return false;
};
}
@@ -115,5 +80,5 @@ export default {
handleLoginIdChanged,
handlePasswordChanged,
handleSuccessfulLogin,
scheduleExpiredNotification,
getSession,
};

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
@@ -11,6 +11,13 @@ import {
handlePasswordChanged,
} from 'app/actions/views/login';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
jest.mock('app/mattermost', () => ({
app: {
setAppCredentials: () => jest.fn(),

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
export function makeDirectChannel(otherUserId, switchToChannel = true) {
export function makeDirectChannel(otherUserId) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
@@ -23,11 +23,11 @@ export function makeDirectChannel(otherUserId, switchToChannel = true) {
dispatch(toggleDMChannel(otherUserId, 'true', channel.id));
} else {
result = await dispatch(createDirectChannel(currentUserId, otherUserId));
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
if (channel && switchToChannel) {
if (channel) {
dispatch(handleSelectChannel(channel.id));
}

View File

@@ -1,11 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import {Posts} from 'mattermost-redux/constants';
import {PostTypes} from 'mattermost-redux/action_types';
import {doPostAction} from 'mattermost-redux/actions/posts';
import {ViewTypes} from 'app/constants';
import {generateId} from 'app/utils/file';
@@ -40,31 +37,3 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
});
};
}
export function setAutocompleteSelector(dataSource, onSelect, options) {
return {
type: ViewTypes.SELECTED_ACTION_MENU,
data: {
dataSource,
onSelect,
options,
},
};
}
export function selectAttachmentMenuAction(postId, actionId, text, value) {
return (dispatch) => {
dispatch({
type: ViewTypes.SUBMIT_ATTACHMENT_MENU_ACTION,
postId,
data: {
[actionId]: {
text,
value,
},
},
});
dispatch(doPostAction(postId, actionId, value));
};
}

View File

@@ -1,17 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
import {fetchMyChannelsAndMembers, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getPosts} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {ViewTypes} from 'app/constants';
import {recordTime} from 'app/utils/segment';
import {handleSelectChannel} from 'app/actions/views/channel';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction,
} from 'app/actions/views/channel';
export function startDataCleanup() {
return async (dispatch, getState) => {
@@ -46,21 +51,16 @@ export function loadConfigAndLicense() {
};
}
export function loadFromPushNotification(notification, startAppFromPushNotification) {
export function loadFromPushNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {channels} = state.entities.channels;
const {currentChannelId, channels} = state.entities.channels;
const channelId = data.channel_id;
let channelId = '';
let teamId = currentTeamId;
if (data) {
channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
teamId = data.team_id || currentTeamId;
}
// when the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
// load any missing data
const loading = [];
@@ -83,7 +83,17 @@ export function loadFromPushNotification(notification, startAppFromPushNotificat
dispatch(selectTeam({id: teamId}));
}
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
// when the notification is from the same channel as the current channel
// we should get the posts
if (channelId === currentChannelId) {
dispatch(markChannelAsRead(channelId, null, false));
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
dispatch(markChannelAsRead(channelId, currentChannelId, false));
dispatch(setChannelDisplayName(''));
dispatch(handleSelectChannel(channelId));
}
};
}
@@ -91,10 +101,8 @@ export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
// A non-optimistic version of the createPost action in mattermost-redux with the file handling
// removed since it's not needed.
export function createPostForNotificationReply(post) {
return async (dispatch, getState) => {
export function createPost(post) {
return (dispatch, getState) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
@@ -108,23 +116,18 @@ export function createPostForNotificationReply(post) {
update_at: timestamp,
};
try {
const data = await Client4.createPost({...newPost, create_at: 0});
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[data.id]: data,
[payload.id]: payload,
},
},
channelId: data.channel_id,
channelId: payload.channel_id,
});
return {data};
} catch (error) {
return {error};
}
});
};
}
@@ -136,13 +139,6 @@ export function recordLoadTime(screenName, category) {
};
}
export function setDeepLinkURL(url) {
return {
type: ViewTypes.SET_DEEP_LINK_URL,
url,
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,

View File

@@ -1,7 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
@@ -13,25 +11,3 @@ export function handleSearchDraftChanged(text) {
}, getState);
};
}
export function showSearchModal(navigator, initialValue = '') {
return (dispatch, getState) => {
const theme = getTheme(getState());
const options = {
screen: 'Search',
animated: true,
backButtonTitle: '',
overrideBackPress: true,
passProps: {
initialValue,
},
navigatorStyle: {
navBarHidden: true,
screenBackgroundColor: theme.centerChannelBg,
},
};
navigator.showModal(options);
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {GeneralTypes} from 'mattermost-redux/action_types';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import configureStore from 'redux-mock-store';
@@ -11,6 +11,13 @@ import {ViewTypes} from 'app/constants';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.SelectServer', () => {

View File

@@ -1,16 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {TeamTypes} from 'mattermost-redux/action_types';
import {getMyTeams} from 'mattermost-redux/actions/teams';
import {RequestStatus} from 'mattermost-redux/constants';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {batchActions} from 'redux-batched-actions';
import {markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {NavigationTypes} from 'app/constants';
import {selectFirstAvailableTeam} from 'app/utils/teams';
export function handleTeamChange(teamId) {
import {setChannelDisplayName} from './channel';
export function handleTeamChange(teamId, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
@@ -18,7 +20,19 @@ export function handleTeamChange(teamId) {
return;
}
dispatch({type: TeamTypes.SELECT_TEAM, data: teamId});
const actions = [setChannelDisplayName(''), {type: TeamTypes.SELECT_TEAM, data: teamId}];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannels = state.views.team.lastChannelForTeam[teamId] || [];
const lastChannelId = lastChannels[0] || '';
const currentChannelId = getCurrentChannelId(state);
markChannelAsViewed(currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
}
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM'), getState);
};
}
@@ -26,34 +40,23 @@ export function selectDefaultTeam() {
return async (dispatch, getState) => {
const state = getState();
const {ExperimentalPrimaryTeam} = getConfig(state);
const {ExperimentalPrimaryTeam} = state.entities.general.config;
const {teams: allTeams, myMembers} = state.entities.teams;
const teams = Object.keys(myMembers).map((key) => allTeams[key]);
let defaultTeam = selectFirstAvailableTeam(teams, ExperimentalPrimaryTeam);
let defaultTeam;
if (ExperimentalPrimaryTeam) {
defaultTeam = teams.find((t) => t.name === ExperimentalPrimaryTeam.toLowerCase());
}
if (!defaultTeam) {
defaultTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
}
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));
} else if (state.requests.teams.getTeams.status === RequestStatus.FAILURE || state.requests.teams.getMyTeams.status === RequestStatus.FAILURE) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
handleTeamChange(defaultTeam.id)(dispatch, getState);
} else {
// If for some reason we reached this point cause of a failure in rehydration or something
// lets fetch the teams one more time to make sure the user does not belong to any team
const {data, error} = await dispatch(getMyTeams());
if (error) {
EventEmitter.emit(NavigationTypes.NAVIGATION_ERROR_TEAMS);
return;
}
if (data) {
defaultTeam = selectFirstAvailableTeam(data, ExperimentalPrimaryTeam);
}
if (defaultTeam) {
dispatch(handleTeamChange(defaultTeam.id));
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}
};
}

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
@@ -11,6 +11,13 @@ import {
handleCommentDraftSelectionChanged,
} from 'app/actions/views/thread';
jest.mock('react-native-fetch-blob/fs', () => ({
dirs: {
DocumentDir: () => jest.fn(),
CacheDir: () => jest.fn(),
},
}));
const mockStore = configureStore([thunk]);
describe('Actions.Views.Thread', () => {

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserTypes} from 'mattermost-redux/action_types';
import {General} from 'mattermost-redux/constants';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
export function setCurrentUserStatusOffline() {
return (dispatch, getState) => {
const currentUserId = getCurrentUserId(getState());
return dispatch({
type: UserTypes.RECEIVED_STATUS,
data: {
user_id: currentUserId,
status: General.OFFLINE,
},
});
};
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {UserTypes} from 'mattermost-redux/action_types';
import {General} from 'mattermost-redux/constants';
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
const mockStore = configureStore([thunk]);
jest.mock('mattermost-redux/actions/users', () => ({
getStatus: (...args) => ({type: 'MOCK_GET_STATUS', args}),
getStatusesByIds: (...args) => ({type: 'MOCK_GET_STATUS_BY_IDS', args}),
startPeriodicStatusUpdates: () => ({type: 'MOCK_PERIODIC_STATUS_UPDATES'}),
}));
describe('Actions.Views.User', () => {
let store;
beforeEach(() => {
store = mockStore({
entities: {
users: {
currentUserId: 'current-user-id',
statuses: {
'current-user-id': 'online',
'another-user-id1': 'away',
'another-user-id2': 'dnd',
},
},
},
});
});
test('should set the current user as offline', async () => {
const action = {
type: UserTypes.RECEIVED_STATUS,
data: {
user_id: 'current-user-id',
status: General.OFFLINE,
},
};
await store.dispatch(setCurrentUserStatusOffline());
expect(store.getActions()).toEqual([action]);
});
});

View File

@@ -1,15 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// See License.txt for license information.
/* eslint-disable global-require*/
import {AsyncStorage, Linking, NativeModules, Platform, Text} from 'react-native';
import {AsyncStorage, NativeModules} from 'react-native';
import {setGenericPassword, getGenericPassword, resetGenericPassword} from 'react-native-keychain';
import {loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {setDeepLinkURL} from 'app/actions/views/root';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
import {getCurrentLocale} from 'app/selectors/i18n';
@@ -51,48 +50,10 @@ export default class App {
this.token = null;
this.url = null;
// Load polyfill for iOS 9
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS < 10) {
require('@babel/polyfill');
}
}
// Usage deeplinking
Linking.addEventListener('url', this.handleDeepLink);
this.setFontFamily();
this.getStartupThemes();
this.getAppCredentials();
}
setFontFamily = () => {
// Set a global font for Android
if (Platform.OS === 'android') {
const defaultFontFamily = {
style: {
fontFamily: 'Roboto',
},
};
const TextRender = Text.render;
const initialDefaultProps = Text.defaultProps;
Text.defaultProps = {
...initialDefaultProps,
...defaultFontFamily,
};
Text.render = function render(props, ...args) {
const oldProps = props;
let newProps = {...props, style: [defaultFontFamily.style, props.style]};
try {
return Reflect.apply(TextRender, this, [newProps, ...args]);
} finally {
newProps = oldProps;
}
};
}
};
getTranslations = () => {
if (this.translations) {
return this.translations;
@@ -130,20 +91,13 @@ export default class App {
const [deviceToken, currentUserId] = usernameParsed;
const [token, url] = passwordParsed;
// if for any case the url and the token aren't valid proceed with re-hydration
if (url && url !== 'undefined' && token && token !== 'undefined') {
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
} else {
this.waitForRehydration = true;
}
this.deviceToken = deviceToken;
this.currentUserId = currentUserId;
this.token = token;
this.url = url;
Client4.setUrl(url);
Client4.setToken(token);
}
} else {
this.waitForRehydration = false;
}
} catch (error) {
return null;
@@ -198,7 +152,6 @@ export default class App {
if (!currentUserId) {
return;
}
const username = `${deviceToken}, ${currentUserId}`;
const password = `${token},${url}`;
@@ -208,14 +161,7 @@ export default class App {
this.url = url;
}
// Only save to keychain if the url and token are set
if (url && token) {
try {
setGenericPassword(username, password);
} catch (e) {
console.warn('could not set credentials', e); //eslint-disable-line no-console
}
}
setGenericPassword(username, password);
};
setStartupThemes = (toolbarBackground, toolbarTextColor, appBackground) => {
@@ -269,11 +215,6 @@ export default class App {
]);
};
handleDeepLink = (event) => {
const {url} = event;
store.dispatch(setDeepLinkURL(url));
}
launchApp = async () => {
const shouldStart = await handleManagedConfig();
if (shouldStart) {
@@ -288,21 +229,11 @@ export default class App {
const {dispatch} = store;
Linking.getInitialURL().then((url) => {
dispatch(setDeepLinkURL(url));
});
let screen = 'SelectServer';
if (this.token && this.url) {
screen = 'Channel';
tracker.initialLoad = Date.now();
try {
dispatch(loadMe());
} catch (e) {
// Fall through since we should have a previous version of the current user because we have a token
console.warn('Failed to load current user when starting on Channel screen', e); // eslint-disable-line no-console
}
dispatch(loadMe());
}
switch (screen) {
@@ -314,6 +245,7 @@ export default class App {
break;
}
this.setStartAppFromPushNotification(false);
this.setAppStarted(true);
}
}

View File

@@ -1,68 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Badge should match snapshot 1`] = `
<View
style={
Array [
Object {
"backgroundColor": "#444",
"borderRadius": 20,
"height": 20,
"padding": 12,
"paddingBottom": 3,
"paddingTop": 3,
"position": "absolute",
"right": 30,
"top": 2,
},
Object {
"backgroundColor": "#ffffff",
},
Object {
"opacity": 0,
},
]
}
>
<View
style={
Object {
"alignItems": "center",
"alignSelf": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<View
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"justifyContent": "center",
"textAlignVertical": "center",
}
}
>
<Text
onLayout={[Function]}
style={
Array [
Object {
"color": "white",
"fontSize": 14,
},
Object {
"color": "#145dbf",
"fontSize": 10,
},
]
}
>
1
</Text>
</View>
</View>
</View>
`;

View File

@@ -1,57 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`profile_picture_button should match snapshot 1`] = `
<AttachmentButton
blurTextBox={[MockFunction]}
browseFileTypes="public.item"
canBrowseFiles={true}
canBrowsePhotoLibrary={true}
canBrowseVideoLibrary={true}
canTakePhoto={true}
canTakeVideo={true}
extraOptions={
Array [
null,
]
}
maxFileCount={5}
maxFileSize={20971520}
navigator={
Object {
"dismissModal": [MockFunction],
"push": [MockFunction],
"setButtons": [MockFunction],
"setOnNavigatorEvent": [MockFunction],
}
}
theme={
Object {
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Mattermost",
}
}
uploadFiles={[MockFunction]}
/>
`;

View File

@@ -1,62 +1,365 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnouncementBanner should match snapshot 1`] = `
<AnimatedComponent
style={
Array [
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
"dismissBanner": [MockFunction],
}
}
>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
allowDismissal={true}
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={true}
bannerText="Banner Text"
bannerTextColor="#fff"
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"fontSize": 14,
"marginRight": 5,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
Banner Text
</Text>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>,
"style": Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"backgroundColor": "#ddd",
"height": 0,
},
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
Banner Text
</Text>,
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>,
],
"onPress": [Function],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "Banner Text",
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": "Banner Text",
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"allowFontScaling": false,
"color": "#fff",
"name": "info",
"size": 16,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
Symbol(enzyme.__nodes__): Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"children": <TouchableOpacity
activeOpacity={0.2}
onPress={[Function]}
style={
Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
}
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
Banner Text
</Text>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>,
"style": Array [
Object {
"overflow": "hidden",
"paddingHorizontal": 10,
"position": "absolute",
"top": 0,
"width": "100%",
},
Object {
"color": "#fff",
"backgroundColor": "#ddd",
"height": 0,
},
]
}
>
<RemoveMarkdown
value="Banner Text"
/>
</Text>
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>
</TouchableOpacity>
</AnimatedComponent>
],
},
"ref": null,
"rendered": Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"activeOpacity": 0.2,
"children": Array [
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
]
}
>
Banner Text
</Text>,
<Icon
allowFontScaling={false}
color="#fff"
name="info"
size={16}
/>,
],
"onPress": [Function],
"style": Object {
"alignItems": "center",
"flex": 1,
"flexDirection": "row",
},
},
"ref": null,
"rendered": Array [
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"accessible": true,
"allowFontScaling": true,
"children": "Banner Text",
"ellipsizeMode": "tail",
"numberOfLines": 1,
"style": Array [
Object {
"flex": 1,
"fontSize": 14,
"marginRight": 5,
},
Object {
"color": "#fff",
},
],
},
"ref": null,
"rendered": "Banner Text",
"type": [Function],
},
Object {
"instance": null,
"key": undefined,
"nodeType": "class",
"props": Object {
"allowFontScaling": false,
"color": "#fff",
"name": "info",
"size": 16,
},
"ref": null,
"rendered": null,
"type": [Function],
},
],
"type": [Function],
},
"type": [Function],
},
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;
exports[`AnnouncementBanner should match snapshot 2`] = `null`;
exports[`AnnouncementBanner should match snapshot 2`] = `
ShallowWrapper {
"length": 1,
Symbol(enzyme.__root__): [Circular],
Symbol(enzyme.__unrendered__): <AnnouncementBanner
actions={
Object {
"dismissBanner": [MockFunction],
}
}
allowDismissal={true}
bannerColor="#ddd"
bannerDismissed={false}
bannerEnabled={false}
bannerText="Banner Text"
bannerTextColor="#fff"
/>,
Symbol(enzyme.__renderer__): Object {
"batchedUpdates": [Function],
"getNode": [Function],
"render": [Function],
"simulateEvent": [Function],
"unmount": [Function],
},
Symbol(enzyme.__node__): null,
Symbol(enzyme.__nodes__): Array [
null,
],
Symbol(enzyme.__options__): Object {
"adapter": ReactSixteenAdapter {
"options": Object {
"enableComponentDidUpdateOnSetState": true,
},
},
},
}
`;

View File

@@ -1,9 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
Animated,
StyleSheet,
Text,
@@ -12,19 +13,19 @@ import {
import {intlShape} from 'react-intl';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import RemoveMarkdown from 'app/components/remove_markdown';
const {View: AnimatedView} = Animated;
export default class AnnouncementBanner extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
dismissBanner: PropTypes.func.isRequired,
}).isRequired,
allowDismissal: PropTypes.bool,
bannerColor: PropTypes.string,
bannerDismissed: PropTypes.bool,
bannerEnabled: PropTypes.bool,
bannerText: PropTypes.string,
bannerTextColor: PropTypes.string,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
};
static contextTypes = {
@@ -51,24 +52,30 @@ export default class AnnouncementBanner extends PureComponent {
}
}
handlePress = () => {
const {navigator, theme} = this.props;
handleDismiss = () => {
const {actions, bannerText} = this.props;
actions.dismissBanner(bannerText);
};
navigator.push({
screen: 'ExpandedAnnouncementBanner',
title: this.context.intl.formatMessage({
id: 'mobile.announcement_banner.title',
defaultMessage: 'Announcement',
}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
handlePress = () => {
const {formatMessage} = this.context.intl;
const options = [{
text: formatMessage({id: 'mobile.announcement_banner.ok', defaultMessage: 'OK'}),
}];
if (this.props.allowDismissal) {
options.push({
text: formatMessage({id: 'mobile.announcement_banner.dismiss', defaultMessage: 'Dismiss'}),
onPress: this.handleDismiss,
});
}
Alert.alert(
formatMessage({id: 'mobile.announcement_banner.title', defaultMessage: 'Announcement'}),
this.props.bannerText,
options,
{cancelable: false}
);
};
toggleBanner = (show = true) => {
@@ -113,7 +120,7 @@ export default class AnnouncementBanner extends PureComponent {
numberOfLines={1}
style={[style.bannerText, bannerTextStyle]}
>
<RemoveMarkdown value={bannerText}/>
{bannerText}
</Text>
<MaterialIcons
color={bannerTextColor}

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import AnnouncementBanner from './announcement_banner.js';
@@ -12,13 +12,15 @@ jest.useFakeTimers();
describe('AnnouncementBanner', () => {
const baseProps = {
actions: {
dismissBanner: jest.fn(),
},
allowDismissal: true,
bannerColor: '#ddd',
bannerDismissed: false,
bannerEnabled: true,
bannerText: 'Banner Text',
bannerTextColor: '#fff',
navigator: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
@@ -26,9 +28,21 @@ describe('AnnouncementBanner', () => {
<AnnouncementBanner {...baseProps}/>
);
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
wrapper.setProps({bannerEnabled: false});
expect(wrapper.getElement()).toMatchSnapshot();
expect(wrapper).toMatchSnapshot();
});
});
test('should call actions.dismissBanner on handleDismiss', () => {
const actions = {dismissBanner: jest.fn()};
const props = {...baseProps, actions};
const wrapper = shallow(
<AnnouncementBanner {...props}/>
);
wrapper.instance().handleDismiss();
expect(actions.dismissBanner).toHaveBeenCalledTimes(1);
expect(actions.dismissBanner).toHaveBeenCalledWith(props.bannerText);
});
});

View File

@@ -1,10 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {dismissBanner} from 'app/actions/views/announcement';
import AnnouncementBanner from './announcement_banner';
@@ -14,13 +16,21 @@ function mapStateToProps(state) {
const {announcement} = state.views;
return {
allowDismissal: config.AllowBannerDismissal === 'true',
bannerColor: config.BannerColor,
bannerDismissed: config.BannerText === announcement,
bannerEnabled: config.EnableBanner === 'true' && license.IsLicensed === 'true',
bannerText: config.BannerText,
bannerTextColor: config.BannerTextColor || '#000',
theme: getTheme(state),
};
}
export default connect(mapStateToProps)(AnnouncementBanner);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
dismissBanner,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AnnouncementBanner);

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
@@ -10,7 +10,6 @@ import {displayUsername} from 'mattermost-redux/utils/user_utils';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
import BottomSheet from 'app/utils/bottom_sheet';
export default class AtMention extends React.PureComponent {
static propTypes = {
@@ -18,6 +17,7 @@ export default class AtMention extends React.PureComponent {
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func.isRequired,
onPostPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
teammateNameDisplay: PropTypes.string,
@@ -74,7 +74,7 @@ export default class AtMention extends React.PureComponent {
};
getUserDetailsFromMentionName(props) {
let mentionName = props.mentionName.toLowerCase();
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {
@@ -95,33 +95,26 @@ export default class AtMention extends React.PureComponent {
}
handleLongPress = async () => {
const {formatMessage} = this.context.intl;
const {intl} = this.context;
const config = await mattermostManaged.getLocalConfig();
let action;
if (config.copyAndPasteProtection !== 'false') {
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
const actionText = formatMessage({id: 'mobile.mention.copy_mention', defaultMessage: 'Copy Mention'});
BottomSheet.showBottomSheetWithOptions({
options: [actionText, cancelText],
cancelButtonIndex: 1,
}, (value) => {
if (value !== 1) {
this.handleCopyMention();
}
});
action = {
text: intl.formatMessage({
id: 'mobile.mention.copy_mention',
defaultMessage: 'Copy Mention',
}),
onPress: this.handleCopyMention,
};
}
this.props.onLongPress(action);
};
handleCopyMention = () => {
const {user} = this.state;
const {mentionName} = this.props;
let username = mentionName;
if (user.username) {
username = user.username;
}
const {username} = this.state;
Clipboard.setString(`@${username}`);
};

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

View File

@@ -1,5 +1,3 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
@@ -10,8 +8,6 @@ import {
StyleSheet,
TouchableOpacity,
} from 'react-native';
import RNFetchBlob from 'rn-fetch-blob';
import Icon from 'react-native-vector-icons/Ionicons';
import {DocumentPicker} from 'react-native-document-picker';
import ImagePicker from 'react-native-image-picker';
@@ -19,58 +15,35 @@ import Permissions from 'react-native-permissions';
import {PermissionTypes} from 'app/constants';
import {changeOpacity} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
const ShareExtension = NativeModules.MattermostShare;
export default class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
browseFileTypes: PropTypes.string,
canBrowseFiles: PropTypes.bool,
canBrowsePhotoLibrary: PropTypes.bool,
canBrowseVideoLibrary: PropTypes.bool,
canTakePhoto: PropTypes.bool,
canTakeVideo: PropTypes.bool,
children: PropTypes.node,
fileCount: PropTypes.number,
maxFileCount: PropTypes.number.isRequired,
maxFileSize: PropTypes.number.isRequired,
navigator: PropTypes.object.isRequired,
onShowFileMaxWarning: PropTypes.func,
onShowFileSizeWarning: PropTypes.func,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired,
wrapper: PropTypes.bool,
extraOptions: PropTypes.arrayOf(PropTypes.object),
};
static defaultProps = {
browseFileTypes: Platform.OS === 'ios' ? 'public.item' : '*/*',
canBrowseFiles: true,
canBrowsePhotoLibrary: true,
canBrowseVideoLibrary: true,
canTakePhoto: true,
canTakeVideo: true,
maxFileCount: 5,
extraOptions: null,
};
static contextTypes = {
intl: intlShape.isRequired,
};
attachPhotoFromCamera = () => {
return this.attachFileFromCamera('photo', 'camera');
};
attachFileFromCamera = async (mediaType, source) => {
attachFileFromCamera = async () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 0.8,
videoQuality: 'high',
quality: 1.0,
noData: true,
mediaType,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true,
@@ -92,7 +65,7 @@ export default class AttachmentButton extends PureComponent {
},
};
const hasPhotoPermission = await this.hasPhotoPermission(source);
const hasPhotoPermission = await this.hasPhotoPermission();
if (hasPhotoPermission) {
ImagePicker.launchCamera(options, (response) => {
@@ -108,7 +81,7 @@ export default class AttachmentButton extends PureComponent {
attachFileFromLibrary = () => {
const {formatMessage} = this.context.intl;
const options = {
quality: 0.8,
quality: 1.0,
noData: true,
permissionDenied: {
title: formatMessage({
@@ -140,14 +113,10 @@ export default class AttachmentButton extends PureComponent {
});
};
attachVideoFromCamera = () => {
return this.attachFileFromCamera('video', 'camera');
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.context.intl;
const options = {
videoQuality: 'high',
quality: 1.0,
mediaType: 'video',
noData: true,
permissionDenied: {
@@ -177,12 +146,11 @@ export default class AttachmentButton extends PureComponent {
};
attachFileFromFiles = async () => {
const {browseFileTypes} = this.props;
const hasPermission = await this.hasStoragePermission();
if (hasPermission) {
DocumentPicker.show({
filetype: [browseFileTypes],
filetype: [Platform.OS === 'ios' ? 'public.item' : '*/*'],
}, async (error, res) => {
if (error) {
return;
@@ -206,11 +174,11 @@ export default class AttachmentButton extends PureComponent {
}
};
hasPhotoPermission = async (source) => {
hasPhotoPermission = async () => {
if (Platform.OS === 'ios') {
const {formatMessage} = this.context.intl;
let permissionRequest;
const hasPermissionToStorage = await Permissions.check(source || 'photo');
const hasPermissionToStorage = await Permissions.check('photo');
switch (hasPermissionToStorage) {
case PermissionTypes.UNDETERMINED:
@@ -295,13 +263,13 @@ export default class AttachmentButton extends PureComponent {
defaultMessage: 'To upload images from your Android device, please change your permission settings.',
}),
[
grantOption,
{
text: formatMessage({
id: 'mobile.android.permission_denied_dismiss',
defaultMessage: 'Dismiss',
}),
},
grantOption,
]
);
return false;
@@ -312,20 +280,8 @@ export default class AttachmentButton extends PureComponent {
return true;
};
uploadFiles = async (files) => {
const file = files[0];
if (!file.fileSize | !file.fileName) {
const path = (file.path || file.uri).replace('file://', '');
const fileInfo = await RNFetchBlob.fs.stat(path);
file.fileSize = fileInfo.size;
file.fileName = fileInfo.filename;
}
if (file.fileSize > this.props.maxFileSize) {
this.props.onShowFileSizeWarning(file.fileName);
} else {
this.props.uploadFiles(files);
}
uploadFiles = (images) => {
this.props.uploadFiles(images);
};
handleFileAttachmentOption = (action) => {
@@ -344,17 +300,7 @@ export default class AttachmentButton extends PureComponent {
};
showFileAttachmentOptions = () => {
const {
canBrowseFiles,
canBrowsePhotoLibrary,
canBrowseVideoLibrary,
canTakePhoto,
canTakeVideo,
fileCount,
maxFileCount,
onShowFileMaxWarning,
extraOptions,
} = this.props;
const {fileCount, maxFileCount, onShowFileMaxWarning} = this.props;
if (fileCount === maxFileCount) {
onShowFileMaxWarning();
@@ -362,77 +308,50 @@ export default class AttachmentButton extends PureComponent {
}
this.props.blurTextBox();
const items = [];
if (canTakePhoto) {
items.push({
action: () => this.handleFileAttachmentOption(this.attachPhotoFromCamera),
const options = {
items: [{
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
text: {
id: t('mobile.file_upload.camera_photo'),
defaultMessage: 'Take Photo',
id: 'mobile.file_upload.camera',
defaultMessage: 'Take Photo or Video',
},
icon: 'camera',
});
}
if (canTakeVideo) {
items.push({
action: () => this.handleFileAttachmentOption(this.attachVideoFromCamera),
text: {
id: t('mobile.file_upload.camera_video'),
defaultMessage: 'Take Video',
},
icon: 'video-camera',
});
}
if (canBrowsePhotoLibrary) {
items.push({
}, {
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
text: {
id: t('mobile.file_upload.library'),
id: 'mobile.file_upload.library',
defaultMessage: 'Photo Library',
},
icon: 'photo',
});
}
}],
};
if (canBrowseVideoLibrary && Platform.OS === 'android') {
items.push({
if (Platform.OS === 'android') {
options.items.push({
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
text: {
id: t('mobile.file_upload.video'),
id: 'mobile.file_upload.video',
defaultMessage: 'Video Library',
},
icon: 'file-video-o',
});
}
if (canBrowseFiles) {
items.push({
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
text: {
id: t('mobile.file_upload.browse'),
defaultMessage: 'Browse Files',
},
icon: 'file',
});
}
if (extraOptions) {
extraOptions.forEach((option) => {
if (option !== null) {
items.push(option);
}
});
}
options.items.push({
action: () => this.handleFileAttachmentOption(this.attachFileFromFiles),
text: {
id: 'mobile.file_upload.browse',
defaultMessage: 'Browse Files',
},
icon: 'file',
});
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
animationType: 'none',
passProps: {
items,
items: options.items,
},
navigatorStyle: {
navBarHidden: true,
@@ -490,3 +409,4 @@ const style = StyleSheet.create({
justifyContent: 'center',
},
});

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
@@ -13,7 +13,6 @@ import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divide
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class AtMention extends PureComponent {
static propTypes = {
@@ -26,8 +25,8 @@ export default class AtMention extends PureComponent {
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
maxListHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
outChannel: PropTypes.array,
@@ -82,7 +81,7 @@ export default class AtMention extends PureComponent {
const sections = [];
if (isSearch) {
sections.push({
id: t('mobile.suggestion.members'),
id: 'mobile.suggestion.members',
defaultMessage: 'Members',
data: teamMembers,
key: 'teamMembers',
@@ -90,7 +89,7 @@ export default class AtMention extends PureComponent {
} else {
if (inChannel.length) {
sections.push({
id: t('suggestion.mention.members'),
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members',
data: inChannel,
key: 'inChannel',
@@ -99,7 +98,7 @@ export default class AtMention extends PureComponent {
if (this.checkSpecialMentions(matchTerm)) {
sections.push({
id: t('suggestion.mention.special'),
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions',
data: this.getSpecialMentions(),
key: 'special',
@@ -109,7 +108,7 @@ export default class AtMention extends PureComponent {
if (outChannel.length) {
sections.push({
id: t('suggestion.mention.nonmembers'),
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel',
data: outChannel,
key: 'outChannel',
@@ -132,18 +131,18 @@ export default class AtMention extends PureComponent {
getSpecialMentions = () => {
return [{
completeHandle: 'all',
id: t('suggestion.mention.all'),
id: 'suggestion.mention.all',
defaultMessage: 'Notifies everyone in the channel, use in {townsquare} to notify the whole team',
values: {
townsquare: this.props.defaultChannel.display_name,
},
}, {
completeHandle: 'channel',
id: t('suggestion.mention.channel'),
id: 'suggestion.mention.channel',
defaultMessage: 'Notifies everyone in the channel',
}, {
completeHandle: 'here',
id: t('suggestion.mention.here'),
id: 'suggestion.mention.here',
defaultMessage: 'Notifies everyone in the channel and online',
}];
};
@@ -204,7 +203,7 @@ export default class AtMention extends PureComponent {
};
render() {
const {maxListHeight, theme} = this.props;
const {isSearch, listHeight, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -219,7 +218,7 @@ export default class AtMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, {maxHeight: maxListHeight}]}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
@@ -235,5 +234,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
listView: {
backgroundColor: theme.centerChannelBg,
},
search: {
minHeight: 125,
},
};
});

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';

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