Compare commits
20 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf7373157 | ||
|
|
a5df05ee59 | ||
|
|
c0fb9aa4ba | ||
|
|
4549ecdd94 | ||
|
|
b9179b0774 | ||
|
|
5d78720c2b | ||
|
|
ebeea7b27f | ||
|
|
43aad265ac | ||
|
|
129d0a5189 | ||
|
|
bb26ce5395 | ||
|
|
c4b48dd647 | ||
|
|
705d13c3ef | ||
|
|
0123c631be | ||
|
|
71f80a033e | ||
|
|
beaac8f373 | ||
|
|
485757996a | ||
|
|
ae94ef24a5 | ||
|
|
43689751dc | ||
|
|
2b1a30d757 | ||
|
|
17296fec36 |
@@ -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
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@(@react-native-async-storage|@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library)/**",
|
||||
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
|
||||
"group": "external",
|
||||
"position": "before"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
8
.github/ISSUE_TEMPLATE.md
vendored
@@ -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 you’re 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
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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) -->
|
||||
|
||||
55
.github/workflows/scorecards-analysis.yml
vendored
@@ -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
@@ -30,7 +30,6 @@ DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.aab
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
|
||||
@@ -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.
|
||||
|
||||
309
NOTICE.txt
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -551,29 +532,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.
|
||||
@@ -655,41 +613,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
|
||||
---
|
||||
|
||||
## 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 +1310,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 +1942,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.
|
||||
@@ -2273,42 +2134,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-math-view
|
||||
|
||||
This product contains 'react-native-math-view' by Shachar.
|
||||
|
||||
A react native view used to easily display and handle math. The library doesn't use WebView.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/ShaMan123/react-native-math-view
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 ShaMan123
|
||||
|
||||
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.
|
||||
© 2022 GitHub, Inc.
|
||||
|
||||
---
|
||||
|
||||
## react-native-mmkv-storage
|
||||
|
||||
This product contains 'react-native-mmkv-storage' by Ammar Ahmed.
|
||||
@@ -2885,39 +2710,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.
|
||||
@@ -3023,65 +2815,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.
|
||||
|
||||
12
README.md
@@ -1,14 +1,14 @@
|
||||
# Mattermost Mobile App
|
||||
[](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).
|
||||
|
||||
|
||||
@@ -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
|
||||
---------------------------
|
||||
|
||||
@@ -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 404
|
||||
versionName "1.53.0"
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 368
|
||||
versionName "1.46.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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
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 +26,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 +39,44 @@ 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))
|
||||
);
|
||||
|
||||
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 +91,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) {
|
||||
@@ -180,41 +124,24 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> list = notificationsInChannel.get(channelId);
|
||||
if (list == null) {
|
||||
list = Collections.synchronizedList(new ArrayList(0));
|
||||
}
|
||||
|
||||
list.add(0, notificationId);
|
||||
if (list.size() > 1) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
list.add(0, notificationId + 1);
|
||||
}
|
||||
|
||||
notificationsInChannel.put(channelId, list);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +149,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,11 +164,19 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +209,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 +221,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) {
|
||||
|
||||
@@ -98,16 +98,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);
|
||||
@@ -155,17 +145,13 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
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, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
setNotificationSound(notification, notificationPreferences);
|
||||
setNotificationVibrate(notification, notificationPreferences);
|
||||
|
||||
@@ -57,6 +57,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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public interface RNEditTextOnPasteListener {
|
||||
void onPaste(Uri itemUri);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 787 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 610 B |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 833 B |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -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>
|
||||
@@ -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
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 7.0 KiB |
|
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
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
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
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 10 KiB |
|
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
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
|
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
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -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>
|
||||
@@ -2,5 +2,4 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#FFFFFF</color>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,75 +3,24 @@
|
||||
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {Client4} from '@client/rest';
|
||||
import {AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppCallRequest, AppContext, AppBinding} from '@mm-redux/types/apps';
|
||||
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {cleanForm, createCallRequest, makeCallErrorResponse} from '@utils/apps';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {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 {showModal} from './navigation';
|
||||
|
||||
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 +29,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 +53,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 +84,33 @@ 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;
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -19,19 +19,19 @@ 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 +118,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>, isRoot = false): Array<GenericAction> {
|
||||
const {myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
@@ -214,8 +214,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 +246,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 +319,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 +328,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 +350,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;
|
||||
|
||||
@@ -5,7 +5,6 @@ import merge from 'deepmerge';
|
||||
import {Keyboard, Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import {CHANNEL} from '@constants/screen';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
@@ -92,7 +91,7 @@ export function resetToChannel(passProps = {}) {
|
||||
}
|
||||
|
||||
export function resetToSelectServer(allowOtherServers) {
|
||||
const theme = Preferences.THEMES.denim;
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
@@ -292,7 +291,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) {
|
||||
@@ -373,34 +372,6 @@ 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;
|
||||
@@ -457,13 +428,6 @@ export function showOverlay(name, passProps, options = {}) {
|
||||
overlay: {
|
||||
interceptTouchOutside: false,
|
||||
},
|
||||
...Platform.select({
|
||||
android: {
|
||||
statusBar: {
|
||||
drawBehind: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Navigation.showOverlay({
|
||||
|
||||
@@ -37,7 +37,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',
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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';
|
||||
@@ -12,7 +10,6 @@ import {ViewTypes} from '@constants';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
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,
|
||||
@@ -36,11 +33,11 @@ import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} f
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
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';
|
||||
|
||||
@@ -232,6 +229,7 @@ export function handleSelectChannel(channelId) {
|
||||
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
|
||||
@@ -389,7 +387,7 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: isCollapsedThreadsEnabled(state) ? unreadMessageCountRoot : unreadMessageCount,
|
||||
count: unreadMessageCount,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
@@ -639,10 +637,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 +745,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,
|
||||
|
||||
@@ -166,15 +166,6 @@ describe('Actions.Views.Channel', () => {
|
||||
[currentTeamId]: {},
|
||||
},
|
||||
},
|
||||
apps: {
|
||||
pluginEnabled: true,
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableLegacySidebar: 'true',
|
||||
},
|
||||
serverVersion: '5.12.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -183,7 +174,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);
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {doAppSubmit, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {doAppCall, 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 {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
@@ -24,6 +24,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 +40,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 +62,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',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {addChannelToCategory} from '@mm-redux/actions/channel_categories';
|
||||
import {createChannel} from '@mm-redux/actions/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
@@ -20,7 +19,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 +37,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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,11 +19,6 @@ export function setCustomStatus(customStatus: UserCustomStatus): ActionFunc {
|
||||
user.props.customStatus = JSON.stringify(customStatus);
|
||||
dispatch({type: UserTypes.RECEIVED_ME, data: user});
|
||||
|
||||
// Server does not like empty 'expires_at' string.
|
||||
if (!customStatus.expires_at) {
|
||||
delete customStatus.expires_at;
|
||||
}
|
||||
|
||||
try {
|
||||
await Client4.updateCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {loadChannelsByTeamName} from '@actions/views/channel';
|
||||
import {getPost as fetchPost, selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import {permalinkBadTeam} from '@utils/general';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
@@ -20,50 +17,26 @@ let showingPermalink = false;
|
||||
|
||||
export function showPermalink(intl: typeof intlShape, teamName: string, postId: string, openAsPermalink = true) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
|
||||
let name = teamName;
|
||||
if (!name) {
|
||||
name = getCurrentTeam(state).name;
|
||||
name = getCurrentTeam(getState()).name;
|
||||
}
|
||||
|
||||
const loadTeam = await dispatch(loadChannelsByTeamName(name, permalinkBadTeam.bind(null, intl)));
|
||||
|
||||
let isThreadPost;
|
||||
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
if (collapsedThreadsEnabled) {
|
||||
let post = getPost(state, postId);
|
||||
if (!post) {
|
||||
const {data} = await dispatch(fetchPost(postId));
|
||||
if (data) {
|
||||
post = data;
|
||||
}
|
||||
}
|
||||
if (post) {
|
||||
isThreadPost = Boolean(post.root_id);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!loadTeam.error) {
|
||||
Keyboard.dismiss();
|
||||
dispatch(selectFocusedPostId(postId));
|
||||
if (showingPermalink) {
|
||||
await dismissAllModals();
|
||||
}
|
||||
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
isThreadPost,
|
||||
focusedPostId: postId,
|
||||
teamName,
|
||||
};
|
||||
|
||||
if (showingPermalink) {
|
||||
Navigation.updateProps(screen, passProps);
|
||||
return {};
|
||||
}
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
|
||||
@@ -40,6 +40,7 @@ export function sendEphemeralPost(message, channelId = '', parentId = '', userId
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: parentId,
|
||||
parent_id: parentId,
|
||||
props: {},
|
||||
};
|
||||
|
||||
@@ -61,6 +62,7 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: postRootId,
|
||||
parent_id: postRootId,
|
||||
props: {
|
||||
username: user.username,
|
||||
addedUsername,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
@@ -9,7 +8,7 @@ import {NavigationTypes, ViewTypes} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getChannelAndMyMember} from '@mm-redux/actions/channels';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {receivedNewPost, selectPost} from '@mm-redux/actions/posts';
|
||||
import {receivedNewPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from '@mm-redux/actions/teams';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
@@ -32,10 +31,7 @@ export function startDataCleanup() {
|
||||
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {general} = state.entities;
|
||||
const actions = [];
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
try {
|
||||
const [config, license] = await Promise.all([
|
||||
@@ -43,31 +39,23 @@ export function loadConfigAndLicense() {
|
||||
Client4.getClientLicenseOld(),
|
||||
]);
|
||||
|
||||
if (!deepEqual(general.config, config)) {
|
||||
actions.push({
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data: config,
|
||||
});
|
||||
}
|
||||
const actions = [{
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data: config,
|
||||
}, {
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data: license,
|
||||
}];
|
||||
|
||||
if (!deepEqual(general.license, license)) {
|
||||
actions.push({
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data: license,
|
||||
});
|
||||
|
||||
if (currentUserId) {
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
if (currentUserId) {
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
|
||||
}
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
|
||||
|
||||
return {config, license};
|
||||
} catch (error) {
|
||||
@@ -76,7 +64,7 @@ export function loadConfigAndLicense() {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFromPushNotification(notification, isInitialNotification, skipChannelSwitch = false) {
|
||||
export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {payload} = notification;
|
||||
@@ -108,15 +96,8 @@ export function loadFromPushNotification(notification, isInitialNotification, sk
|
||||
await Promise.all(loading);
|
||||
}
|
||||
|
||||
if (!skipChannelSwitch) {
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
dispatch(selectPost(''));
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
|
||||
const {root_id: rootId} = notification.payload || {};
|
||||
if (isCollapsedThreadsEnabled(state) && rootId) {
|
||||
dispatch(selectPost(rootId));
|
||||
}
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
const sortByNewest = (a, b) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
@@ -21,7 +23,7 @@ export function scheduleExpiredNotification(intl) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
const config = getConfig(state);
|
||||
|
||||
if (config.ExtendSessionLengthWithActivity === 'true') {
|
||||
if (isMinimumServerVersion(Client4.serverVersion, 5, 24) && config.ExtendSessionLengthWithActivity === 'true') {
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
return;
|
||||
}
|
||||
|
||||