Compare commits

..

12 Commits

Author SHA1 Message Date
Weblate (bot)
ca308dee84 Translations update from Weblate (#5532)
* Translated using Weblate (German)

Currently translated at 99.7% (707 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/de/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/bg/

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/bg/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/tr/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/nl/

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/en_AU/

* Translated using Weblate (German)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/de/

Translated using Weblate (German)

Currently translated at 99.5% (706 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/de/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/es/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (709 of 709 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/

Co-authored-by: jprusch <rs@schaeferbarthold.de>
Co-authored-by: Nikolai Zahariev <nikolaiz@yahoo.com>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
Co-authored-by: Markus Hermann <markus.hermann@uni-marburg.de>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
2021-07-13 09:44:16 +02:00
Mattermost Build
d640a5643d Bump app build number to 363 (#5537) (#5538)
(cherry picked from commit d7cd660ab7)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-12 20:07:30 -04:00
Mattermost Build
da575c7cb1 1.45 Release testing bug fixes (#5529) (#5536)
* MM-36908 display unicode emoji in text field after selecting from autocomple

* MM-36935 Fix android crash when ammending search in Sidebar Jump to

* MM-36929 & MM-36928 fix notification badge resetting on new notification

* MM-36920 Fix android push notification issues

* MM-36922 Edit profile image

* Fix crash when opening Android test notification

(cherry picked from commit 78156ee75b)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-12 16:13:54 -04:00
Mattermost Build
962a1759a5 MM-36881 Fix scroll perf degradation with pull to refresh enabled (#5525) (#5535)
* Fix scroll perf degradation with pull to refresh enabled

* Update app/components/post_list/post_list_refresh_control.tsx

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
(cherry picked from commit 58d9c2e9f0)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-12 16:13:42 -04:00
Mattermost Build
50f616f5d7 MM-36900 Fix mm emoji (#5526) (#5528) 2021-07-10 12:34:26 +02:00
Mattermost Build
1a20677eba Bump app build number to 362 (#5522) (#5523)
(cherry picked from commit 9fafc3be5b)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-07 21:25:40 -04:00
Mattermost Build
5b5b7bc620 Bring back refresh control for channels and threads (#5511) (#5520)
(cherry picked from commit 073b836beb)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-07 13:04:07 -04:00
Mattermost Build
456ab53ecf Bump Version to 1.45.0 and build to 361 (#5518) (#5519)
* Bump app build number to  361

* Bump app version number to  1.45.0

(cherry picked from commit 6984923a4c)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-07 12:56:07 -04:00
Mattermost Build
9c87835802 Load other teams you can join when deleting cache (#5516) (#5517)
(cherry picked from commit 883399f13d)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-07 12:38:03 -04:00
Mattermost Build
11cc2e57f5 MM-36808 fix auto responder posts (#5505) (#5515) 2021-07-07 06:54:31 -04:00
Mattermost Build
ac5914ef15 MM-36745 Fix animation for showModalOverCurrentContext (#5507) (#5510)
(cherry picked from commit df26a363bc)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-06 14:31:15 -04:00
Mattermost Build
9c15ae1c5b use user skin preferences for emoji (#5502) (#5509) 2021-07-06 20:13:23 +02:00
1662 changed files with 66008 additions and 71336 deletions

View File

@@ -14,7 +14,7 @@ executors:
NODE_ENV: production
BABEL_ENV: production
docker:
- image: circleci/android:api-30-node
- image: circleci/android:api-29-node
working_directory: ~/mattermost-mobile
resource_class: <<parameters.resource_class>>
@@ -24,7 +24,7 @@ executors:
NODE_ENV: production
BABEL_ENV: production
macos:
xcode: "13.0.0"
xcode: "12.1.0"
working_directory: ~/mattermost-mobile
shell: /bin/bash --login -o pipefail

1
.env
View File

@@ -1,3 +1,2 @@
STORYBOOK_PORT=
STORYBOOK_HOST=
RUNNING_E2E=

View File

@@ -7,8 +7,7 @@
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"mattermost",
"import"
"mattermost"
],
"settings": {
"react": {
@@ -48,39 +47,7 @@
"@typescript-eslint/no-use-before-define": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/member-delimiter-style": 2,
"import/order": [
2,
{
"groups": ["builtin", "external", "parent", "sibling", "index", "type"],
"newlines-between": "always",
"pathGroups": [
{
"pattern": "@(@react-native-async-storage|@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library)/**",
"group": "external",
"position": "before"
},
{
"pattern": "@{**,*/**}",
"group": "external",
"position": "after"
},
{
"pattern": "app/**",
"group": "parent",
"position": "before"
}
],
"alphabetize": {
"order": "asc",
"caseInsensitive": true
},
"pathGroupsExcludedImportTypes": ["type"]
}
],
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error"
"@typescript-eslint/explicit-module-boundary-types": "off"
},
"overrides": [
{

View File

@@ -23,9 +23,10 @@ node_modules/react-native/flow/
[options]
emoji=true
exact_by_default=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
format.bracket_spacing=false
exact_by_default=true
module.file_ext=.js
module.file_ext=.json
@@ -63,4 +64,4 @@ untyped-import
untyped-type-import
[version]
^0.162.0
^0.137.0

View File

@@ -1,12 +1,12 @@
Per Mattermost guidelines, GitHub issues are for bug reports: <https://handbook.mattermost.com/contributors/contributors/ways-to-contribute>.
Per Mattermost guidelines, GitHub issues are for bug reports: <http://www.mattermost.org/filing-issues/>.
For troubleshooting see: https://forum.mattermost.com/.
For feature proposals see: https://www.mattermost.com/feature-ideas/
For troubleshooting see: http://forum.mattermost.org/.
For feature proposals see: http://www.mattermost.org/feature-requests/
If you've found a bug--something appears unintentional--please follow these steps:
1. Confirm youre filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://mattermost.com/security-vulnerability-report/).
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://about.mattermost.com/report-security-issue/).
3. [File a new issue](https://github.com/mattermost/mattermost-mobile/issues/new) using the format below. Mattermost will confirm steps to reproduce and file in Jira, or ask for more details if there is trouble reproducing it. If there's already an existing bug in Jira, it will be linked back to the GitHub issue so you can track when it gets fixed.
#### Summary

View File

@@ -26,7 +26,6 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
- [ ] Added or updated unit tests (required for all new features)
- [ ] Has UI changes
- [ ] Includes text changes and localization file updates
- [ ] Have tested against the 5 core themes to ensure consistency between them.
#### Device Information
This PR was tested on: <!-- Device name(s), OS version(s) -->

View File

@@ -1,55 +0,0 @@
name: Scorecards supply-chain security
on:
# Only the default branch is supported.
branch_protection_rule:
schedule:
- cron: '22 14 * * 1'
push:
branches: [ master ]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
actions: read
contents: read
steps:
- name: "Checkout code"
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@c8416b0b2bf627c349ca92fc8e3de51a64b005cf # v1.0.2
with:
results_file: results.sarif
results_format: sarif
# Read-only PAT token. To create it,
# follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
# Publish the results to enable scorecard badges. For more details, see
# https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories, `publish_results` will automatically be set to `false`,
# regardless of the value entered here.
publish_results: true
# Upload the results as artifacts (optional).
- name: "Upload artifact"
uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26
with:
sarif_file: results.sarif

1
.gitignore vendored
View File

@@ -30,7 +30,6 @@ DerivedData
*.hmap
*.ipa
*.apk
*.aab
*.xcuserstate
project.xcworkspace
ios/Pods

View File

@@ -1,4 +1,4 @@
Submit feature requests to https://www.mattermost.com/feature-ideas/. File non-security related bugs here in the following format:
Submit feature requests to http://www.mattermost.org/feature-requests/. File non-security related bugs here in the following format:
#### Summary
Issue in one concise sentence.

View File

@@ -7,58 +7,39 @@ NOTICES:
This document includes a list of open source components used in Mattermost Mobile, including those that have been modified.
--------
## @babel/runtime
## @mattermost/react-native-paste-input
This product contains 'runtime' by Sebastian McKenzie.
This product contains '@mattermost/react-native-paste-input' by Mattermost.
React Native TextInput component have functionality to capture text input from a user by using the soft and hardware keyboards but lacks the ability to restrict copy & paste options as well as allowing pasting different files formats copied from other apps, like images & videos from the Photos gallery app.
babel's modular runtime helpers
* HOMEPAGE:
* https://github.com/mattermost/react-native-paste-input
* https://github.com/babel/babel/tree/master/packages/babel-runtime
* LICENSE: MIT
MIT License
Copyright (c) 2020 Elias Nahum
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## msgpack/msgpack
This product contains 'msgpack/msgpack' by MessagePack.
MessagePack is an efficient binary serialization format. It's like JSON. but fast and small.
* HOMEPAGE:
* https://github.com/msgpack/msgpack
* LICENSE: ISC
Copyright 2019 The MessagePack Community.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
@@ -167,29 +148,6 @@ SOFTWARE.
---
## @react-native-community/datetimepicker
This product contains '@react-native-community/datetimepicker' by React Native Community.
React Native date & time picker component for iOS, Android and Windows.
* HOMEPAGE:
* https://github.com/react-native-datetimepicker/datetimepicker
* LICENSE: MIT
MIT License
Copyright (c) 2019 React Native Community
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## @react-native-community/masked-view
This product contains '@react-native-community/masked-view' by React Native Community.
@@ -551,29 +509,6 @@ SOFTWARE.
---
## @types/redux-mock-store
This product contains '@types/redux-mock-store' by Redux.
A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.
* HOMEPAGE:
* https://github.com/reduxjs/redux-mock-store
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2017 Arnaud Benard
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## analytics-react-native
This product contains a modified version of 'analytics-react-native' by Segment.
@@ -595,101 +530,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## array.prototype.flat
This product contains a modified version of 'array.prototype.flat' by ECMAScript Shims.
An ES2019 spec-compliant Array.prototype.flat shim/polyfill/replacement that works as far down as ES3.
* HOMEPAGE:
* https://github.com/es-shims/Array.prototype.flat
* LICENSE: MIT
MIT License
Copyright (c) 2017 ECMAScript Shims
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## base-64
This product contains a modified version of 'base-64' by Dan Kogai.
Yet another Base64 transcoder.
* HOMEPAGE:
* https://github.com/dankogai/js-base64
* LICENSE: BSD 3-Clause "New" or "Revised" License
Copyright (c) 2014, Dan Kogai All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of {{{project}}} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## buffer
This product contains a modified version of 'buffer' by Feross Aboukhadijeh.
The buffer module from node.js, for the browser.
* HOMEPAGE:
* https://github.com/feross/buffer
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) Feross Aboukhadijeh, and other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## commonmark
This product contains a modified version of 'commonmark' by John MacFarlane.
@@ -1387,41 +1227,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---
## pako
This product contains 'pako' by Nodeca.
zlib port to javascript, very fast!
* HOMEPAGE:
* https://github.com/nodeca/pako
* LICENSE: MIT
(The MIT License)
Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## prop-types
This product contains 'prop-types' by Facebook.
@@ -2054,33 +1859,6 @@ SOFTWARE.
---
## react-native-incall-manager
This product contains a modified version of 'react-native-incall-manager' by React Native WebRTC.
Handling media-routes/sensors/events during a audio/video chat on React Native
* HOMEPAGE:
* https://github.com/react-native-webrtc/react-native-incall-manager
* LICENSE: ISC License
Copyright (c) 2016, zxcpoiu
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## react-native-keyboard-aware-scroll-view
This product contains a modified version of 'react-native-keyboard-aware-scroll-view' by APSL.
@@ -2849,39 +2627,6 @@ SOFTWARE.
---
## react-native-webrtc
This product contains a modified version of 'react-native-webrtc'.
* HOMEPAGE:
* https://github.com/react-native-webrtc/react-native-webrtc
* LICENSE: MIT
The MIT License (MIT)
Copyright (c) 2015 Howard Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-webview
This product contains a modified version of 'react-native-webview' by Jamon Holmgren.
@@ -2987,65 +2732,6 @@ SOFTWARE.
---
## readable-stream
This product contains 'readable-stream' by Node.js.
Node.js core streams for userland
* HOMEPAGE:
* https://github.com/nodejs/readable-stream
* LICENSE: Node.js is licensed for use as follows:
"""
Copyright Node.js contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
This license applies to parts of Node.js originating from the
https://github.com/joyent/node repository:
"""
Copyright Joyent, Inc. and other Node contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
"""
---
## redux
This product contains 'redux' by Redux.

View File

@@ -1,14 +1,14 @@
# Mattermost Mobile App
[![Mattermost](https://user-images.githubusercontent.com/7205829/136108314-75cd2e1f-4147-4cfa-a16c-9b3b0313ea25.png)](https://mattermost.com)
# Mattermost Mobile
- **Minimum Server versions:** Current ESR version (6.3.0)
- **Supported iOS versions:** 12.1+
- **Minimum Server versions:** Current ESR version (5.31.3)
- **Supported iOS versions:** 11+
- **Supported Android versions:** 7.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. This repo is for the mobile app that runs on Android and iOS. 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://developers.mattermost.com/contribute/mobile/build-your-own/).
New features are released monthly - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for currently-supported features!
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).

View File

@@ -17,7 +17,7 @@ Security updates
Mattermost has a mandatory upgrade policy, and updates are only provided for the latest release. Critical updates are delivered as dot releases. Details on security updates are announced 30 days after the availability of the update.
For more details about the security content of past releases, see the [Security Updates](https://mattermost.com/security-updates/) page on the Mattermost website. For timely notifications about new security updates, subscribe to the [Security Bulletins Mailing List](https://mattermost.com/security-updates/#sign-up).
For more details about the security content of past releases, see the [Security Updates](https://mattermost.com/security-updates/) page on the Mattermost website. For timely notifications about new security updates, subscribe to the [Security Bulletins Mailing List](https://about.mattermost.com/security-bulletin).
Contributing to this policy
---------------------------

View File

@@ -113,26 +113,27 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and that value will be read here. If it is not set
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for in debug.
*/
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 397
versionName "1.51.2"
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 363
versionName "1.45.0"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -164,11 +165,6 @@ android {
debug {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
if (nativeArchitectures) {
ndk {
abiFilters nativeArchitectures.split(',')
}
}
}
unsigned.initWith(buildTypes.release)
unsigned {
@@ -199,11 +195,6 @@ android {
pickFirst '**/*.so'
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
repositories {
@@ -239,7 +230,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
@@ -278,7 +269,7 @@ dependencies {
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.implementation
from configurations.compile
into 'libs'
}

View File

@@ -9,10 +9,6 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".MainApplication"
@@ -82,13 +78,5 @@
</intent-filter>
</activity>
</application>
<queries>
<intent>
<action android:name="com.google.android.youtube.api.service.START" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -8,11 +8,13 @@ import java.util.Collections;
import java.util.List;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
import com.ammarahmed.mmkv.RNMMKVModule;
public class CustomMMKVJSIModulePackage extends ReanimatedJSIModulePackage {
@Override
public List<JSIModuleSpec> getJSIModules(ReactApplicationContext reactApplicationContext, JavaScriptContextHolder jsContext) {
super.getJSIModules(reactApplicationContext, jsContext);
reactApplicationContext.getNativeModule(RNMMKVModule.class).installLib(jsContext, reactApplicationContext.getFilesDir().getAbsolutePath() + "/mmkv");
return Collections.emptyList();
}

View File

@@ -1,20 +1,16 @@
package com.mattermost.rnbeta;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
@@ -29,11 +25,11 @@ import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_
import com.mattermost.react_native_interface.ResolvePromise;
import org.json.JSONArray;
import org.json.JSONObject;
public class CustomPushNotification extends PushNotification {
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
private static final String PUSH_TYPE_MESSAGE = "message";
private static final String PUSH_TYPE_CLEAR = "clear";
private static final String PUSH_TYPE_SESSION = "session";
@@ -42,95 +38,46 @@ public class CustomPushNotification extends PushNotification {
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = String.valueOf(pInfo.versionCode);
String storedVersion = null;
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
if (pSharedPref != null) {
storedVersion = pSharedPref.getString("Version", "");
}
if (!version.equals(storedVersion)) {
if (pSharedPref != null) {
SharedPreferences.Editor editor = pSharedPref.edit();
editor.putString("Version", version);
editor.apply();
}
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
saveNotificationsMap(context, inputMap);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
if (!android.text.TextUtils.isEmpty(channelId)) {
final String notificationIdStr = notificationId.toString();
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
if (notifications == null) {
return;
}
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.cancel(notificationId);
notifications.remove(notificationIdStr);
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
boolean hasMore = false;
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.getString("root_id").equals(rootId);
} else if (isCRTEnabled) {
hasMore = !bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.getString("channel_id").equals(channelId);
}
if (hasMore) {
break;
}
}
if (!hasMore) {
notificationsInChannel.remove(groupId);
} else {
notificationsInChannel.put(groupId, notifications);
}
notifications.remove(notificationId);
saveNotificationsMap(context, notificationsInChannel);
}
}
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
if (!android.text.TextUtils.isEmpty(channelId)) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(notificationId);
}
}
// rootId is available only when CRT is enabled & clearing the thread
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
String groupId = isClearThread ? rootId : channelId;
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
public static void clearChannelNotifications(Context context, String channelId) {
if (!android.text.TextUtils.isEmpty(channelId)) {
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
List<Integer> notifications = notificationsInChannel.get(channelId);
if (notifications == null) {
return;
}
notificationsInChannel.remove(groupId);
notificationsInChannel.remove(channelId);
saveNotificationsMap(context, notificationsInChannel);
notifications.forEach(
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
);
if (context != null) {
for (final Integer notificationId : notifications) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(notificationId);
}
}
}
}
public static void clearAllNotifications(Context context) {
if (context != null) {
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
notificationsInChannel.clear();
saveNotificationsMap(context, notificationsInChannel);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
@@ -145,8 +92,6 @@ public class CustomPushNotification extends PushNotification {
final String ackId = initialData.getString("ack_id");
final String postId = initialData.getString("post_id");
final String channelId = initialData.getString("channel_id");
final String rootId = initialData.getString("root_id");
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
@@ -176,53 +121,28 @@ public class CustomPushNotification extends PushNotification {
case PUSH_TYPE_MESSAGE:
case PUSH_TYPE_SESSION:
if (!mAppLifecycleFacade.isAppVisible()) {
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
if (type.equals(PUSH_TYPE_MESSAGE)) {
if (channelId != null) {
try {
JSONObject post = new JSONObject();
if (!android.text.TextUtils.isEmpty(rootId)) {
post.put("root_id", rootId);
}
if (!android.text.TextUtils.isEmpty(postId)) {
post.put("post_id", postId);
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
synchronized (notificationsInChannel) {
List<Integer> list = notificationsInChannel.get(channelId);
if (list == null) {
list = Collections.synchronizedList(new ArrayList(0));
}
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
final String groupId = isThreadNotification ? rootId : channelId;
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
if (notifications == null) {
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
}
if (notifications.size() > 0) {
createSummary = false;
}
notifications.put(String.valueOf(notificationId), post);
if (createSummary) {
// Add the summary notification id as well
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
}
notificationsInChannel.put(groupId, notifications);
list.add(0, notificationId);
notificationsInChannel.put(channelId, list);
saveNotificationsMap(mContext, notificationsInChannel);
} catch(Exception e) {
e.printStackTrace();
}
}
}
buildNotification(notificationId, createSummary);
super.postNotification(notificationId);
} else {
notifyReceivedToJS();
}
break;
case PUSH_TYPE_CLEAR:
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
clearChannelNotifications(mContext, channelId);
break;
}
@@ -237,33 +157,26 @@ public class CustomPushNotification extends PushNotification {
Bundle data = mNotificationProps.asBundle();
final String channelId = data.getString("channel_id");
final String rootId = data.getString("root_id");
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
final String postId = data.getString("post_id");
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
}
if (channelId != null) {
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
List<Integer> notifications = notificationsInChannel.get(channelId);
notifications.remove(notificationId);
saveNotificationsMap(mContext, notificationsInChannel);
clearChannelNotifications(mContext, channelId);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = super.getCTAPendingIntent();
final Notification notification = buildNotification(pendingIntent);
if (createSummary) {
final Notification summary = getNotificationSummaryBuilder(pendingIntent).build();
super.postNotification(summary, notificationId + 1);
}
super.postNotification(notification, notificationId);
}
@Override
protected NotificationCompat.Builder getNotificationBuilder(PendingIntent intent) {
Bundle bundle = mNotificationProps.asBundle();
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false);
}
protected NotificationCompat.Builder getNotificationSummaryBuilder(PendingIntent intent) {
Bundle bundle = mNotificationProps.asBundle();
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle);
}
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
@@ -274,7 +187,7 @@ public class CustomPushNotification extends PushNotification {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
if (pSharedPref != null && context != null) {
JSONObject json = new JSONObject(inputMap);
@@ -286,41 +199,23 @@ public class CustomPushNotification extends PushNotification {
}
}
/**
* Map Structure
*
* {
* channel_id1 | thread_id1: {
* notification_id1: {
* post_id: 'p1',
* root_id: 'r1',
* }
* }
* }
*
*/
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
Map<String, List<Integer>> outputMap = new HashMap<>();
if (context != null) {
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
try {
if (pSharedPref != null) {
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
JSONObject json = new JSONObject(jsonString);
// Can be a channel_id or thread_id
Iterator<String> groupIdsItr = json.keys();
while (groupIdsItr.hasNext()) {
String groupId = groupIdsItr.next();
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
Map<String, JSONObject> notifications = new HashMap<>();
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
while(notificationIdKeys.hasNext()) {
String notificationId = notificationIdKeys.next();
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
notifications.put(notificationId, post);
Iterator<String> keysItr = json.keys();
while (keysItr.hasNext()) {
String key = keysItr.next();
JSONArray array = json.getJSONArray(key);
List<Integer> values = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
values.add(array.getInt(i));
}
outputMap.put(groupId, notifications);
outputMap.put(key, values);
}
}
} catch (Exception e) {

View File

@@ -80,10 +80,7 @@ public class CustomPushNotificationHelper {
.setName(senderName);
try {
Bitmap avatar = userAvatar(context, senderId);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, senderId))));
} catch (IOException e) {
e.printStackTrace();
}
@@ -98,16 +95,6 @@ public class CustomPushNotificationHelper {
userInfoBundle = new Bundle();
}
String postId = bundle.getString("post_id");
if (postId != null) {
userInfoBundle.putString("post_id", postId);
}
String rootId = bundle.getString("root_id");
if (rootId != null) {
userInfoBundle.putString("root_id", rootId);
}
String channelId = bundle.getString("channel_id");
if (channelId != null) {
userInfoBundle.putString("channel_id", channelId);
@@ -150,22 +137,18 @@ public class CustomPushNotificationHelper {
.addAction(replyAction);
}
public static NotificationCompat.Builder createNotificationBuilder(Context context, PendingIntent intent, Bundle bundle, boolean createSummary) {
public static NotificationCompat.Builder createNotificationBuilder(Context context, PendingIntent intent, Bundle bundle) {
final NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_HIGH_IMPORTANCE_ID);
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(context);
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationGroup(notification, groupId, createSummary);
setNotificationGroup(notification, channelId);
setNotificationBadgeType(notification);
setNotificationSound(notification, notificationPreferences);
setNotificationVibrate(notification, notificationPreferences);
@@ -259,10 +242,7 @@ public class CustomPushNotificationHelper {
.setName("Me");
try {
Bitmap avatar = userAvatar(context, "me");
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, "me"))));
} catch (IOException e) {
e.printStackTrace();
}
@@ -393,13 +373,10 @@ public class CustomPushNotificationHelper {
notification.setStyle(messagingStyle);
}
private static void setNotificationGroup(NotificationCompat.Builder notification, String channelId, boolean setAsSummary) {
notification.setGroup(channelId);
if (setAsSummary) {
// if this is the first notification for the channel then set as summary, otherwise skip
notification.setGroupSummary(true);
}
private static void setNotificationGroup(NotificationCompat.Builder notification, String channelId) {
notification
.setGroup(channelId)
.setGroupSummary(true);
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
@@ -413,10 +390,7 @@ public class CustomPushNotificationHelper {
if (channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(context, senderId);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
notification.setLargeIcon(userAvatar(context, senderId));
} catch (IOException e) {
e.printStackTrace();
}

View File

@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.RestrictionsManager;
import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.io.File;
import java.util.HashMap;
import java.util.List;
@@ -57,6 +58,7 @@ private final ReactNativeHost mReactNativeHost =
// Packages that cannot be auto linked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new RNNotificationsPackage(MainApplication.this));
packages.add(new RNPasteableTextInputPackage());
packages.add(
new TurboReactPackage() {
@Override
@@ -99,7 +101,7 @@ private final ReactNativeHost mReactNativeHost =
@Override
protected JSIModulePackage getJSIModulePackage() {
return (JSIModulePackage) new CustomMMKVJSIModulePackage();
return new CustomMMKVJSIModulePackage();
}
};

View File

@@ -19,9 +19,6 @@ public class NotificationDismissService extends IntentService {
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
final String rootId = bundle.getString("root_id");
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
if (postId != null) {
notificationId = postId.hashCode();
@@ -29,7 +26,7 @@ public class NotificationDismissService extends IntentService {
notificationId = channelId.hashCode();
}
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
CustomPushNotification.cancelNotification(context, channelId, notificationId);
Log.i("ReactNative", "Dismiss notification");
}
}

View File

@@ -118,10 +118,6 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
WritableMap map = Arguments.createMap();
Notification n = sbn.getNotification();
Bundle bundle = n.extras;
String postId = bundle.getString("post_id");
map.putString("post_id", postId);
String rootId = bundle.getString("root_id");
map.putString("root_id", rootId);
String channelId = bundle.getString("channel_id");
map.putString("channel_id", channelId);
result.pushMap(map);
@@ -130,9 +126,8 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
}
@ReactMethod
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
public void removeDeliveredNotifications(String channelId) {
final Context context = mApplication.getApplicationContext();
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
CustomPushNotification.clearChannelNotifications(context, channelId);
}
}

View File

@@ -9,13 +9,11 @@ import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import java.io.IOException;
import java.util.Objects;
import okhttp3.Call;
import okhttp3.MediaType;
@@ -23,6 +21,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.internal.annotations.EverythingIsNonNull;
import org.json.JSONObject;
import org.json.JSONException;
@@ -89,19 +88,21 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
@EverythingIsNonNull
public void onFailure(Call call, IOException e) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
@EverythingIsNonNull
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
assert response.body() != null;
Log.i("ReactNative", String.format("Reply FAILED status %s BODY %s", response.code(), Objects.requireNonNull(response.body()).string()));
Log.i("ReactNative", String.format("Reply FAILED status %s BODY %s", response.code(), response.body().string()));
onReplyFailed(notificationId);
}
}
@@ -132,7 +133,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
final Intent cta = new Intent(mContext, ProxyService.class);
final PushNotificationProps notificationProps = new PushNotificationProps(bundle);
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, cta, notificationProps);
NotificationCompat.Builder builder = CustomPushNotificationHelper.createNotificationBuilder(mContext, pendingIntent, bundle, false);
NotificationCompat.Builder builder = CustomPushNotificationHelper.createNotificationBuilder(mContext, pendingIntent, bundle);
Notification notification = builder.build();
NotificationCompat.MessagingStyle messagingStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(notification);
assert messagingStyle != null;

View File

@@ -0,0 +1,7 @@
package com.mattermost.rnbeta;
import android.net.Uri;
public interface RNEditTextOnPasteListener {
void onPaste(Uri itemUri);
}

View File

@@ -0,0 +1,98 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
public class RNPasteableActionCallback implements ActionMode.Callback {
private final RNPasteableEditText mEditText;
RNPasteableActionCallback(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
Bundle config = MainApplication.instance.getManagedConfig();
if (config != null) {
WritableMap result = Arguments.fromBundle(config);
String copyPasteProtection = result.getString("copyAndPasteProtection");
assert copyPasteProtection != null;
if (copyPasteProtection.equals("true")) {
disableMenus(menu);
}
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Uri uri = this.getUriInClipboard();
if (item.getItemId() == android.R.id.paste && uri != null) {
mEditText.getOnPasteListener().onPaste(uri);
mode.finish();
} else {
mEditText.onTextContextMenuItem(item.getItemId());
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
}
private void disableMenus(Menu menu) {
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
int id = item.getItemId();
boolean shouldDisableMenu = (
id == android.R.id.paste
|| id == android.R.id.copy
|| id == android.R.id.cut
);
item.setEnabled(!shouldDisableMenu);
}
}
private Uri getUriInClipboard() {
ClipboardManager clipboardManager = (ClipboardManager) mEditText.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return null;
}
ClipData.Item item = clipData.getItemAt(0);
if (item == null) {
return null;
}
CharSequence chars = item.getText();
if (chars == null) {
return null;
}
String text = chars.toString();
if (text.length() > 0) {
return null;
}
return item.getUri();
}
}

View File

@@ -0,0 +1,22 @@
package com.mattermost.rnbeta;
import android.content.Context;
import com.facebook.react.views.textinput.ReactEditText;
public class RNPasteableEditText extends ReactEditText {
private RNEditTextOnPasteListener mOnPasteListener;
public RNPasteableEditText(Context context) {
super(context);
}
public void setOnPasteListener(RNEditTextOnPasteListener listener) {
mOnPasteListener = listener;
}
public RNEditTextOnPasteListener getOnPasteListener() {
return mOnPasteListener;
}
}

View File

@@ -0,0 +1,163 @@
package com.mattermost.rnbeta;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.util.Patterns;
import android.webkit.MimeTypeMap;
import android.webkit.URLUtil;
import androidx.annotation.RequiresApi;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.mattermost.share.RealPathUtil;
import com.mattermost.share.ShareModule;
import java.io.FileNotFoundException;
import java.io.File;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Matcher;
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
private final RNPasteableEditText mEditText;
RNPasteableEditTextOnPasteListener(RNPasteableEditText editText) {
mEditText = editText;
}
@Override
public void onPaste(Uri itemUri) {
ReactContext reactContext = (ReactContext)mEditText.getContext();
String uri = itemUri.toString();
WritableArray images = null;
WritableMap error = null;
String uriMimeType = reactContext.getContentResolver().getType(itemUri);
if (uriMimeType == null) {
return;
}
// Special handle for Google docs
if (uri.equals("content://com.google.android.apps.docs.editors.kix.editors.clipboard")) {
ClipboardManager clipboardManager = (ClipboardManager) reactContext.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboardManager.getPrimaryClip();
if (clipData == null) {
return;
}
ClipData.Item item = clipData.getItemAt(0);
String htmlText = item.getHtmlText();
// Find uri from html
Matcher matcher = Patterns.WEB_URL.matcher(htmlText);
if (matcher.find()) {
uri = htmlText.substring(matcher.start(1), matcher.end());
}
}
if (uri.startsWith("http")) {
Thread pastImageFromUrlThread = new Thread(new RNPasteableImageFromUrl(reactContext, mEditText, uri));
pastImageFromUrlThread.start();
return;
}
uri = RealPathUtil.getRealPathFromURI(reactContext, itemUri);
if (uri == null) {
return;
}
// Get type
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
if (extension == null) {
return;
}
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType == null) {
return;
}
// Get fileName
String fileName = URLUtil.guessFileName(uri, null, mimeType);
if (uri.contains(ShareModule.CACHE_DIR_NAME) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
uri = moveToImagesCache(uri, fileName);
}
if (uri == null) {
return;
}
// Get fileSize
long fileSize;
try {
ContentResolver contentResolver = reactContext.getContentResolver();
AssetFileDescriptor assetFileDescriptor = contentResolver.openAssetFileDescriptor(itemUri, "r");
if (assetFileDescriptor == null) {
return;
}
fileSize = assetFileDescriptor.getLength();
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", "file://" + uri);
images = Arguments.createArray();
images.pushMap(image);
} catch (FileNotFoundException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mEditText.getId(),
"onPaste",
event
);
}
@RequiresApi(api = Build.VERSION_CODES.O)
private String moveToImagesCache(String src, String fileName) {
ReactContext ctx = (ReactContext)mEditText.getContext();
String cacheFolder = ctx.getCacheDir().getAbsolutePath() + "/Images/";
String dest = cacheFolder + fileName;
File folder = new File(cacheFolder);
try {
if (!folder.exists()) {
boolean created = folder.mkdirs();
if (!created) {
return null;
}
}
Files.move(Paths.get(src), Paths.get(dest));
} catch (FileAlreadyExistsException fileError) {
// Do nothing and return dest path
} catch (Exception err) {
return null;
}
return dest;
}
}

View File

@@ -0,0 +1,76 @@
package com.mattermost.rnbeta;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.textinput.ReactEditText;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
public class RNPasteableImageFromUrl implements Runnable {
private final ReactContext mContext;
private final String mUri;
private final ReactEditText mTarget;
RNPasteableImageFromUrl(ReactContext context, ReactEditText target, String uri) {
mContext = context;
mUri = uri;
mTarget = target;
}
@Override
public void run() {
WritableArray images = null;
WritableMap error = null;
try {
URL url = new URL(mUri);
URLConnection u = url.openConnection();
// Get type
String mimeType = u.getHeaderField("Content-Type");
if (!mimeType.startsWith("image")) {
return;
}
// Get fileSize
long fileSize = Long.parseLong(u.getHeaderField("Content-Length"));
// Get fileName
String contentDisposition = u.getHeaderField("Content-Disposition");
int startIndex = contentDisposition.indexOf("filename=\"") + 10;
int endIndex = contentDisposition.length() - 1;
String fileName = contentDisposition.substring(startIndex, endIndex);
WritableMap image = Arguments.createMap();
image.putString("type", mimeType);
image.putDouble("fileSize", fileSize);
image.putString("fileName", fileName);
image.putString("uri", mUri);
images = Arguments.createArray();
images.pushMap(image);
} catch (IOException e) {
error = Arguments.createMap();
error.putString("message", e.getMessage());
}
WritableMap event = Arguments.createMap();
event.putArray("data", images);
event.putMap("error", error);
mContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(
mTarget.getId(),
"onPaste",
event
);
}
}

View File

@@ -0,0 +1,85 @@
package com.mattermost.rnbeta;
import androidx.annotation.NonNull;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.textinput.ReactEditText;
import com.facebook.react.views.textinput.ReactTextInputManager;
import java.util.Map;
import javax.annotation.Nullable;
public class RNPasteableTextInputManager extends ReactTextInputManager {
@Override
@NonNull
public String getName() {
return "PasteableTextInputAndroid";
}
@Override
@NonNull
public ReactEditText createViewInstance(ThemedReactContext context) {
RNPasteableEditText editText = new RNPasteableEditText(context) {
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
EditorInfoCompat.setContentMimeTypes(editorInfo,
new String [] {"image/*"});
final InputConnectionCompat.OnCommitContentListener callback =
(inputContentInfo, flags, opts) -> {
// read and display inputContentInfo asynchronously
if ((flags &
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
}
catch (Exception e) {
return false;
}
}
this.getOnPasteListener().onPaste(inputContentInfo.getContentUri());
return true;
};
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
}
};
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setCustomInsertionActionModeCallback(new RNPasteableActionCallback(editText));
editText.setCustomSelectionActionModeCallback(new RNPasteableActionCallback(editText));
return editText;
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, ReactEditText editText) {
super.addEventEmitters(reactContext, editText);
RNPasteableEditText pasteableEditText = (RNPasteableEditText)editText;
pasteableEditText.setOnPasteListener(new RNPasteableEditTextOnPasteListener(pasteableEditText));
}
@Nullable
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
Map<String, Object> map = super.getExportedCustomBubblingEventTypeConstants();
assert map != null;
map.put(
"onPaste",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onPaste")));
return map;
}
}

View File

@@ -0,0 +1,28 @@
package com.mattermost.rnbeta;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
public class RNPasteableTextInputPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Arrays.asList(
new RNPasteableTextInputManager()
);
}
}

BIN
android/app/src/main/res/drawable-hdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 KiB

BIN
android/app/src/main/res/drawable-mdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

BIN
android/app/src/main/res/drawable-xhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -1,32 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/splashscreen_bg"
android:orientation="vertical">
android:background="#ffffff"
android:gravity="center_horizontal"
tools:context=".SplashScreenActivity">
<ImageView
android:id="@+id/splashBackgroundImage"
android:id="@+id/imgLogo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/splash_background"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/splashIcon"
android:layout_width="96dp"
android:layout_height="96dp"
android:contentDescription="@string/app_name"
android:src="@drawable/splash"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</androidx.constraintlayout.widget.ConstraintLayout>

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 1.0 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 B

After

Width:  |  Height:  |  Size: 847 B

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 847 B

After

Width:  |  Height:  |  Size: 1.2 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFF</color>
<color name="transparent">#00000000</color>
<color name="splashscreen_bg">#1E325C</color>
</resources>

View File

@@ -2,5 +2,4 @@
<resources>
<color name="white">#FFFFFF</color>
<color name="transparent">#00000000</color>
<color name="splashscreen_bg">#FFFFFF</color>
</resources>
</resources>

View File

@@ -3,8 +3,8 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowBackground">@color/splashscreen_bg</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>

View File

@@ -1,8 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="files" path="." />
<external-files-path name="external_files" path="." />
<external-path name="external" path="." />
<cache-path name="cache" path="." />
<root-path name="root" path="." />
</paths>

View File

@@ -2,25 +2,26 @@
buildscript {
ext {
buildToolsVersion = "30.0.2"
buildToolsVersion = "29.0.3"
minSdkVersion = 24
compileSdkVersion = 30
targetSdkVersion = 30
compileSdkVersion = 29
targetSdkVersion = 29
supportLibVersion = "28.0.0"
kotlinVersion = "1.5.30"
kotlinVersion = "1.3.61"
firebaseVersion = "21.0.0"
RNNKotlinVersion = kotlinVersion
ndkVersion = "21.4.7075529"
ndkVersion = "21.1.6352462"
}
repositories {
jcenter()
google()
mavenCentral()
mavenLocal()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.gms:google-services:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
// NOTE: Do not place your application dependencies here; they belong
@@ -31,9 +32,9 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenLocal()
mavenCentral()
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url("$rootDir/../node_modules/react-native/android")
@@ -48,16 +49,12 @@ allprojects {
// prebuilt libv8android.so
// url("$rootDir/../node_modules/v8-android/dist")
}
mavenCentral {
// We don't want to fetch react-native from Maven Central as there are
// older versions over there.
content {
excludeGroup "com.facebook.react"
}
}
maven {
url "https://www.jitpack.io"
}
maven {
url ("https://dl.bintray.com/rudderstack/rudderstack")
}
maven {
url "$rootDir/../node_modules/detox/Detox-android"
}

View File

@@ -9,7 +9,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048M
@@ -30,4 +30,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.99.0
FLIPPER_VERSION=0.75.1

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-7.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@@ -1,77 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {sendEphemeralPost} from '@actions/views/post';
import {Client4} from '@client/rest';
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
import {AppCallResponse, AppCallRequest, AppContext, AppBinding} from '@mm-redux/types/apps';
import {CommandArgs} from '@mm-redux/types/integrations';
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
import {Post} from '@mm-redux/types/posts';
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
export function handleBindingClick<Res=unknown>(binding: AppBinding, context: AppContext, intl: any): ActionFunc {
return async (dispatch: DispatchFunc) => {
// Fetch form
if (binding.form?.source) {
const callRequest = createCallRequest(
binding.form.source,
context,
);
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
import {handleGotoLocation} from '@mm-redux/actions/integrations';
import {showModal} from './navigation';
import {Theme} from '@mm-redux/types/preferences';
import CompassIcon from '@components/compass_icon';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import EphemeralStore from '@store/ephemeral_store';
import {makeCallErrorResponse} from '@utils/apps';
import {sendEphemeralPost} from '@actions/views/post';
import {CommandArgs} from '@mm-redux/types/integrations';
const res = await dispatch(doAppFetchForm<Res>(callRequest, intl));
return res;
}
// Open form
if (binding.form) {
// This should come properly formed, but using preventive checks
if (!binding.form?.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.malformed_binding',
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
});
return {error: makeCallErrorResponse(errMsg)};
}
const res: AppCallResponse = {
type: AppCallResponseTypes.FORM,
form: binding.form,
};
return {data: res};
}
// Submit binding
// This should come properly formed, but using preventive checks
if (!binding.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.malformed_binding',
defaultMessage: 'This binding is not properly formed. Contact the App developer.',
});
return {error: makeCallErrorResponse(errMsg)};
}
const callRequest = createCallRequest(
binding.submit,
context,
);
const res = await dispatch(doAppSubmit<Res>(callRequest, intl));
return res;
};
}
export function doAppSubmit<Res=unknown>(inCall: AppCallRequest, intl: any): ActionFunc {
return async () => {
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
return async (dispatch, getState) => {
try {
const call: AppCallRequest = {
...inCall,
context: {
...inCall.context,
track_as_submit: true,
},
};
const res = await Client4.executeAppCall(call, true) as AppCallResponse<Res>;
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;
switch (responseType) {
@@ -80,15 +30,18 @@ export function doAppSubmit<Res=unknown>(inCall: AppCallRequest, intl: any): Act
case AppCallResponseTypes.ERROR:
return {error: res};
case AppCallResponseTypes.FORM: {
if (!res.form?.submit) {
if (!res.form) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
defaultMessage: 'Response type is `form`, but no form was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
cleanForm(res.form);
const screen = EphemeralStore.getNavigationTopComponentId();
if (type === AppCallTypes.SUBMIT && screen !== 'AppForm') {
showAppForm(res.form, call, getTheme(getState()));
}
return {data: res};
}
@@ -101,6 +54,16 @@ export function doAppSubmit<Res=unknown>(inCall: AppCallRequest, intl: any): Act
return {error: makeCallErrorResponse(errMsg)};
}
if (type !== AppCallTypes.SUBMIT) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_submit',
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
});
return {error: makeCallErrorResponse(errMsg)};
}
dispatch(handleGotoLocation(res.navigate_to_url, intl));
return {data: res};
default: {
const errMsg = intl.formatMessage({
@@ -122,72 +85,40 @@ export function doAppSubmit<Res=unknown>(inCall: AppCallRequest, intl: any): Act
};
}
export function doAppFetchForm<Res=unknown>(call: AppCallRequest, intl: any): ActionFunc {
return async () => {
try {
const res = await Client4.executeAppCall(call, false) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;
const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) => {
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
switch (responseType) {
case AppCallResponseTypes.ERROR:
return {error: res};
case AppCallResponseTypes.FORM:
if (!res.form?.submit) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no valid form was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
cleanForm(res.form);
return {data: res};
default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {type: responseType});
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error: any) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {error: makeCallErrorResponse(errMsg)};
let submitButtons = [{
id: 'submit-form',
showAsAction: 'always',
text: 'Submit',
}];
if (form.submit_buttons) {
const options = form.fields.find((f) => f.name === form.submit_buttons)?.options;
const newButtons = options?.map((o) => {
return {
id: 'submit-form_' + o.value,
showAsAction: 'always',
text: o.label,
};
});
if (newButtons && newButtons.length > 0) {
submitButtons = newButtons;
}
}
const options = {
topBar: {
leftButtons: [{
id: 'close-dialog',
icon: closeButton,
}],
rightButtons: submitButtons,
},
};
}
export function doAppLookup<Res=unknown>(call: AppCallRequest, intl: any): ActionFunc {
return async () => {
try {
const res = await Client4.executeAppCall(call, false) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;
switch (responseType) {
case AppCallResponseTypes.OK:
return {data: res};
case AppCallResponseTypes.ERROR:
return {error: res};
default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {type: responseType});
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error: any) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {error: makeCallErrorResponse(errMsg)};
}
};
}
const passProps = {form, call};
showModal('AppForm', form.title, passProps, options);
};
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
return (dispatch: DispatchFunc) => {

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceTypes} from '@constants';
import {DeviceTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch, getState) => {

View File

@@ -4,8 +4,10 @@
/* eslint-disable no-import-assign */
import {Client4} from '@client/rest';
import {PreferenceTypes} from '@mm-redux/action_types';
import {Preferences} from '@mm-redux/constants';
import {PreferenceTypes} from '@mm-redux/action_types';
import * as CommonSelectors from '@mm-redux/selectors/entities/common';
import * as PreferenceSelectors from '@mm-redux/selectors/entities/preferences';
import * as PreferenceUtils from '@mm-redux/utils/preference_utils';

View File

@@ -1,37 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
import {Client4} from '@client/rest';
import {General, Preferences} from '@mm-redux/constants';
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {getMyPreferences} from '@mm-redux/selectors/entities/preferences';
import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
import {ActionResult, GenericAction} from '@mm-redux/types/actions';
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
import {PreferenceType} from '@mm-redux/types/preferences';
import {GlobalState} from '@mm-redux/types/store';
import {UserProfile} from '@mm-redux/types/users';
import {RelationOneToMany} from '@mm-redux/types/utilities';
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels';
import {buildPreference} from '@utils/preferences';
export async function loadSidebarDirectMessagesProfiles(state: GlobalState, channels: Channel[], channelMembers: ChannelMembership[]) {
export async function loadSidebarDirectMessagesProfiles(state: GlobalState, channels: Array<Channel>, channelMembers: Array<ChannelMembership>) {
const currentUserId = getCurrentUserId(state);
const usersInChannel: RelationOneToMany<Channel, UserProfile> = getUserIdsInChannels(state);
const directChannels = Object.values(channels).filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
const prefs: PreferenceType[] = [];
const prefs: Array<PreferenceType> = [];
const promises: Array<Promise<ActionResult>> = []; //only fetch profiles that we don't have and the Direct channel should be visible
const actions = [];
const userIds: string[] = [];
const userIds: Array<string> = [];
// Prepare preferences and start fetching profiles to batch them
directChannels.forEach((c) => {
const profileIds = Array.from(usersInChannel[c.id] || []);
const profilesInChannel: string[] = profileIds.filter((u: string) => u !== currentUserId);
const profilesInChannel: Array<string> = profileIds.filter((u: string) => u !== currentUserId);
userIds.push(...profilesInChannel);
switch (c.type) {
@@ -118,7 +120,7 @@ export async function fetchMyChannelMember(channelId: string) {
}
}
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: string[], isRoot = false): GenericAction[] {
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>): Array<GenericAction> {
const {myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
@@ -127,7 +129,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
data: {
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
},
}, {
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
@@ -135,7 +136,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
teamId,
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
myMembers[channelId].notify_props.mark_unread === General.MENTION,
},
@@ -148,7 +148,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
teamId,
channelId,
amount: 1,
amountRoot: isRoot ? 1 : 0,
},
});
}
@@ -214,8 +213,8 @@ export async function makeGroupMessageVisibleIfNecessary(state: GlobalState, cha
}
}
export async function fetchChannelAndMyMember(channelId: string): Promise<GenericAction[]> {
const actions: GenericAction[] = [];
export async function fetchChannelAndMyMember(channelId: string): Promise<Array<GenericAction>> {
const actions: Array<GenericAction> = [];
try {
const [channel, member] = await Promise.all([
@@ -246,9 +245,9 @@ export async function fetchChannelAndMyMember(channelId: string): Promise<Generi
return actions;
}
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<GenericAction[]> {
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<Array<GenericAction>> {
const userIds: string[] = [];
const actions: GenericAction[] = [];
const actions: Array<GenericAction> = [];
for (const preference of preferences) {
if (preference.category === Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true') {
@@ -319,7 +318,7 @@ export function lastChannelIdForTeam(state: GlobalState, teamId: string): string
return firstChannel;
}
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: ChannelMembership[], profilesInChannel: string[]) {
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
const currentUserId = getCurrentUserId(state);
const myPreferences = getMyPreferences(state);
const users = getUsers(state);
@@ -328,8 +327,7 @@ function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel,
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
const otherUser = users[otherUserId];
const dmVisible = isDirectChannelVisible(currentUserId, myPreferences, channel);
const {serverVersion} = state.entities.general;
const dmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, otherUser ? otherUser.delete_at : 0, currentChannelId, undefined, serverVersion);
const dmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, otherUser ? otherUser.delete_at : 0, currentChannelId);
const member = channelMembers.find((cm) => cm.channel_id === channel.id);
const dmIsUnread = member ? member.mention_count > 0 : false;
const dmFetchProfile = dmIsUnread || (dmVisible && !dmAutoClosed);
@@ -351,14 +349,12 @@ function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel,
return {preferences};
}
function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel, channelMembers: ChannelMembership[], profilesInChannel: string[]) {
function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
const currentUserId = getCurrentUserId(state);
const myPreferences = getMyPreferences(state);
const config = getConfig(state);
const gmVisible = isGroupChannelVisible(myPreferences, channel);
const {serverVersion} = state.entities.general;
const currentChannelId = getCurrentChannelId(state);
const gmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, 0, currentChannelId, undefined, serverVersion);
const gmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, 0);
const channelMember = channelMembers.find((cm) => cm.channel_id === channel.id);
let hasMentions = false;
let isUnread = false;

View File

@@ -1,16 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import merge from 'deepmerge';
import {Keyboard, Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import merge from 'deepmerge';
import CompassIcon from '@components/compass_icon';
import {DeviceTypes, NavigationTypes} from '@constants';
import {CHANNEL} from '@constants/screen';
import {Preferences} from '@mm-redux/constants';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import EventEmmiter from '@mm-redux/utils/event_emitter';
import {DeviceTypes, NavigationTypes} from '@constants';
import {CHANNEL} from '@constants/screen';
import EphemeralStore from '@store/ephemeral_store';
import Store from '@store/store';
@@ -92,7 +92,7 @@ export function resetToChannel(passProps = {}) {
}
export function resetToSelectServer(allowOtherServers) {
const theme = Preferences.THEMES.denim;
const theme = Preferences.THEMES.default;
EphemeralStore.clearNavigationComponents();
@@ -292,7 +292,7 @@ export function showModal(name, title, passProps = {}, options = {}) {
}
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
const title = passProps.title || '';
const title = '';
let animations;
switch (Platform.OS) {
@@ -318,20 +318,14 @@ export function showModalOverCurrentContext(name, passProps = {}, options = {})
default:
animations = {
showModal: {
enter: {
enabled: false,
},
exit: {
enabled: false,
},
enabled: false,
enter: {},
exit: {},
},
dismissModal: {
enter: {
enabled: false,
},
exit: {
enabled: false,
},
enabled: false,
enter: {},
exit: {},
},
};
break;
@@ -373,40 +367,12 @@ export function showSearchModal(initialValue = '') {
showModal(name, title, passProps, options);
}
export const showAppForm = async (form, context, theme) => {
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
let submitButtons;
const customSubmitButtons = form.submit_buttons && form.fields.find((f) => f.name === form.submit_buttons)?.options;
if (!customSubmitButtons?.length) {
submitButtons = [{
id: 'submit-form',
showAsAction: 'always',
text: 'Submit',
}];
}
const options = {
topBar: {
leftButtons: [{
id: 'close-dialog',
icon: closeButton,
}],
rightButtons: submitButtons,
},
};
const passProps = {form, context};
showModal('AppForm', form.title, passProps, options);
};
export async function dismissModal(options = {}) {
if (!EphemeralStore.hasModalsOpened()) {
return;
}
const componentId = options.componentId || EphemeralStore.getNavigationTopComponentId();
const componentId = EphemeralStore.getNavigationTopComponentId();
try {
await Navigation.dismissModal(componentId, options);
@@ -457,13 +423,6 @@ export function showOverlay(name, passProps, options = {}) {
overlay: {
interceptTouchOutside: false,
},
...Platform.select({
android: {
statusBar: {
drawBehind: true,
},
},
}),
};
Navigation.showOverlay({

View File

@@ -1,19 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import merge from 'deepmerge';
import {Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import merge from 'deepmerge';
import EventEmitter from '@mm-redux/utils/event_emitter';
import * as NavigationActions from '@actions/navigation';
import {NavigationTypes} from '@constants';
import Preferences from '@mm-redux/constants/preferences';
import EventEmitter from '@mm-redux/utils/event_emitter';
import EphemeralStore from '@store/ephemeral_store';
import intitialState from '@store/initial_state';
import Store from '@store/store';
import {NavigationTypes} from '@constants';
jest.unmock('@actions/navigation');
const mockStore = configureMockStore([thunk]);
@@ -37,7 +38,7 @@ describe('@actions/navigation', () => {
const topComponentId = 'top-component-id';
const name = 'name';
const title = 'title';
const theme = Preferences.THEMES.denim;
const theme = Preferences.THEMES.default;
const passProps = {
testProp: 'prop',
};
@@ -313,20 +314,14 @@ describe('@actions/navigation', () => {
},
animations: {
showModal: {
enter: {
enabled: false,
},
exit: {
enabled: false,
},
enabled: false,
enter: {},
exit: {},
},
dismissModal: {
enter: {
enabled: false,
},
exit: {
enabled: false,
},
enabled: false,
enter: {},
exit: {},
},
},
};

View File

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

View File

@@ -1,18 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {batchActions} from 'redux-batched-actions';
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
import {Client4} from '@client/rest';
import {ViewTypes} from '@constants';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
import {ViewTypes} from 'app/constants';
import {ChannelTypes, RoleTypes, GroupTypes} from '@mm-redux/action_types';
import {fetchAppBindings} from '@mm-redux/actions/apps';
import {fetchMyCategories} from '@mm-redux/actions/channel_categories';
import {
fetchMyChannelsAndMembers,
getChannelByName,
@@ -20,8 +13,12 @@ import {
leaveChannel as serviceLeaveChannel,
} from '@mm-redux/actions/channels';
import {savePreferences} from '@mm-redux/actions/preferences';
import {getLicense} from '@mm-redux/selectors/entities/general';
import {addUserToTeam, getTeamByName, removeUserFromTeam, selectTeam} from '@mm-redux/actions/teams';
import {Client4} from '@client/rest';
import {General, Preferences} from '@mm-redux/constants';
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {
getCurrentChannelId,
getRedirectChannelNameForTeam,
@@ -29,22 +26,21 @@ import {
getMyChannelMemberships,
isManuallyUnread,
} from '@mm-redux/selectors/entities/channels';
import {getLicense} from '@mm-redux/selectors/entities/general';
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
import {getChannelReachable} from '@selectors/channel';
import {getViewingGlobalThreads} from '@selectors/threads';
import telemetry, {PERF_MARKERS} from '@telemetry';
import {appsEnabled} from '@utils/apps';
import {shouldShowLegacySidebar} from '@utils/categories';
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue, privateChannelJoinPrompt} from '@utils/channels';
import {isPendingPost} from '@utils/general';
import {handleNotViewingGlobalThreadsScreen} from './threads';
import {fetchAppBindings} from '@mm-redux/actions/apps';
import {appsEnabled} from '@utils/apps';
const MAX_RETRIES = 3;
@@ -128,11 +124,9 @@ export function fetchPostActionWithRetry(action, maxTries = MAX_RETRIES) {
export function selectInitialChannel(teamId) {
return (dispatch, getState) => {
const state = getState();
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
if (!collapsedThreadsEnabled || (collapsedThreadsEnabled && !getViewingGlobalThreads(state))) {
const channelId = lastChannelIdForTeam(state, teamId);
dispatch(handleSelectChannel(channelId));
}
const channelId = lastChannelIdForTeam(state, teamId);
dispatch(handleSelectChannel(channelId));
};
}
@@ -225,13 +219,11 @@ export function handleSelectChannel(channelId) {
teamId: channel.team_id || currentTeamId,
},
});
if (getViewingGlobalThreads(state)) {
actions.push(handleNotViewingGlobalThreadsScreen());
}
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
if (appsEnabled(state)) {
//TODO improve sync method
dispatch(fetchAppBindings(currentUserId, channelId));
}
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
@@ -384,12 +376,11 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
if (channel) {
const unreadMessageCount = channel.total_msg_count - member.msg_count;
const unreadMessageCountRoot = channel.total_msg_count_root - member.msg_count_root;
actions.push({
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
data: {
channelId,
count: isCollapsedThreadsEnabled(state) ? unreadMessageCountRoot : unreadMessageCount,
count: unreadMessageCount,
},
}, {
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
@@ -397,7 +388,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
teamId: channel.team_id,
channelId,
amount: unreadMessageCount,
amountRoot: unreadMessageCountRoot,
},
}, {
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
@@ -405,7 +395,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
teamId: channel.team_id,
channelId,
amount: member.mention_count,
amountRoot: member.mention_count_root,
},
});
}
@@ -426,7 +415,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
teamId: prevChannel.team_id,
channelId: prevChannelId,
amount: prevChannel.total_msg_count - prevMember.msg_count,
amountRoot: prevChannel.total_msg_count_root - prevMember.msg_count_root,
},
}, {
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
@@ -434,7 +422,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
teamId: prevChannel.team_id,
channelId: prevChannelId,
amount: prevMember.mention_count,
amountRoot: prevMember.mention_count_root,
},
});
}
@@ -639,10 +626,11 @@ function loadGroupData(isReconnect = false) {
const actions = [];
const team = getCurrentTeam(state);
const currentUserId = getCurrentUserId(state);
const serverVersion = state.entities.general.serverVersion;
const license = getLicense(state);
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
if (hasLicense && team) {
if (hasLicense && team && isMinimumServerVersion(serverVersion, 5, 24)) {
for (let i = 0; i <= MAX_RETRIES; i++) {
try {
if (team.group_constrained) {
@@ -746,10 +734,6 @@ export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect =
}
}
if (!shouldShowLegacySidebar(state)) {
await dispatch(fetchMyCategories(teamId));
}
if (data.channels) {
actions.push({
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,

View File

@@ -4,13 +4,14 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import testHelper from 'test/test_helper';
import * as ChannelActions from '@actions/views/channel';
import {ViewTypes} from '@constants';
import {ChannelTypes} from '@mm-redux/action_types';
import {General} from '@mm-redux/constants';
import postReducer from '@mm-redux/reducers/entities/posts';
import initialState from '@store/initial_state';
import testHelper from '@test/test_helper';
import {General} from '@mm-redux/constants';
const {
handleSelectChannel,
@@ -166,15 +167,6 @@ describe('Actions.Views.Channel', () => {
[currentTeamId]: {},
},
},
apps: {
pluginEnabled: true,
},
general: {
config: {
EnableLegacySidebar: 'true',
},
serverVersion: '5.12.0',
},
},
};
@@ -183,7 +175,7 @@ describe('Actions.Views.Channel', () => {
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
const appChannelSelectors = require('@selectors/channel');
const appChannelSelectors = require('app/selectors/channel');
const getChannelReachableOriginal = appChannelSelectors.getChannelReachable;
appChannelSelectors.getChannelReachable = jest.fn(() => true);

View File

@@ -3,16 +3,18 @@
import {intlShape} from 'react-intl';
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
import {IntegrationTypes} from '@mm-redux/action_types';
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
import {AppCallResponse} from '@mm-redux/types/apps';
import {CommandArgs} from '@mm-redux/types/integrations';
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
import {appsEnabled} from '@utils/apps';
import {AppCallResponse} from '@mm-redux/types/apps';
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
@@ -24,6 +26,7 @@ export function executeCommand(message: string, channelId: string, rootId: strin
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId,
};
let msg = message;
@@ -39,21 +42,21 @@ export function executeCommand(message: string, channelId: string, rootId: strin
const appsAreEnabled = appsEnabled(state);
if (appsAreEnabled) {
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.team_id, args.root_id);
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.root_id);
if (parser.isAppCommand(msg)) {
const {creq, errorMessage} = await parser.composeCommandSubmitCall(msg);
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
const createErrorMessage = (errMessage: string) => {
return {error: {message: errMessage}};
};
if (!creq) {
if (!call) {
return createErrorMessage(errorMessage!);
}
const res = await dispatch(doAppSubmit(creq, intl));
const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
if (res.error) {
const errorResponse = res.error as AppCallResponse;
return createErrorMessage(errorResponse.text || intl.formatMessage({
return createErrorMessage(errorResponse.error || intl.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error.',
}));
@@ -61,19 +64,13 @@ export function executeCommand(message: string, channelId: string, rootId: strin
const callResp = res.data as AppCallResponse;
switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.text) {
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.text, args));
if (callResp.markdown) {
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
}
return {data: {}};
case AppCallResponseTypes.FORM:
return {data: {
form: callResp.form,
call: creq,
}};
case AppCallResponseTypes.NAVIGATE:
return {data: {
goto_location: callResp.navigate_to_url,
}};
return {data: {}};
default:
return createErrorMessage(intl.formatMessage({
id: 'apps.error.responses.unknown_type',

View File

@@ -1,15 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {addChannelToCategory} from '@mm-redux/actions/channel_categories';
import {handleSelectChannel, setChannelDisplayName} from './channel';
import {createChannel} from '@mm-redux/actions/channels';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {cleanUpUrlable} from '@mm-redux/utils/channel_utils';
import {generateId} from '@mm-redux/utils/helpers';
import {handleSelectChannel, setChannelDisplayName} from './channel';
export function generateChannelNameFromDisplayName(displayName) {
let name = cleanUpUrlable(displayName);
@@ -20,7 +18,7 @@ export function generateChannelNameFromDisplayName(displayName) {
return name;
}
export function handleCreateChannel(displayName, purpose, header, type, categoryId) {
export function handleCreateChannel(displayName, purpose, header, type) {
return async (dispatch, getState) => {
const state = getState();
const currentUserId = getCurrentUserId(state);
@@ -38,10 +36,6 @@ export function handleCreateChannel(displayName, purpose, header, type, category
if (data && data.id) {
dispatch(setChannelDisplayName(displayName));
dispatch(handleSelectChannel(data.id));
if (categoryId) {
dispatch(addChannelToCategory(categoryId, data.id));
}
}
};
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {generateChannelNameFromDisplayName} from '@actions/views/create_channel';
import {generateChannelNameFromDisplayName} from 'app/actions/views/create_channel';
describe('Actions.Views.CreateChannel', () => {
describe('generateChannelNameFromDisplayName', () => {

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