forked from Ivasoft/mattermost-mobile
Compare commits
74 Commits
release-1.
...
v1.31.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f337b3690e | ||
|
|
700b3d1a2f | ||
|
|
0618ad432b | ||
|
|
680ead2f57 | ||
|
|
ae49f65458 | ||
|
|
7c933ed656 | ||
|
|
5279014271 | ||
|
|
1eb244b16f | ||
|
|
47d58c96cf | ||
|
|
618d3ffe88 | ||
|
|
543a334977 | ||
|
|
d17e94eae3 | ||
|
|
a01bdd6fc4 | ||
|
|
613ddc1250 | ||
|
|
19a282ad41 | ||
|
|
af580f0e69 | ||
|
|
a1ee76b0c4 | ||
|
|
fbaeb033c9 | ||
|
|
71ed64a14a | ||
|
|
6db738fecb | ||
|
|
edfa0257c3 | ||
|
|
c36da73181 | ||
|
|
4dd83521ed | ||
|
|
ed690d5c47 | ||
|
|
028dbf1d5a | ||
|
|
6cdb50ed39 | ||
|
|
ae5cdb56a3 | ||
|
|
8afde65922 | ||
|
|
9a53918f57 | ||
|
|
8e314022ca | ||
|
|
2b5def9a90 | ||
|
|
c19f73a8a7 | ||
|
|
7158050f1a | ||
|
|
67ac6ec8a5 | ||
|
|
97175667f1 | ||
|
|
b3d274b6c7 | ||
|
|
bb557ff4fa | ||
|
|
1c92529958 | ||
|
|
e84f7196a0 | ||
|
|
bcb4197ea8 | ||
|
|
1454dc918c | ||
|
|
f4e180d339 | ||
|
|
43c42caba7 | ||
|
|
e1a9c47a58 | ||
|
|
9434acd79a | ||
|
|
9f578461e4 | ||
|
|
968507d8c9 | ||
|
|
7f3ff62609 | ||
|
|
ecee2074a1 | ||
|
|
9e4c790b2d | ||
|
|
19db2ebcc7 | ||
|
|
19d6aaa932 | ||
|
|
0e2bc65d70 | ||
|
|
3a887b446a | ||
|
|
7f450e9532 | ||
|
|
f407bd0568 | ||
|
|
c48a5e3b61 | ||
|
|
7e0ca73c28 | ||
|
|
6008e6bae2 | ||
|
|
9a0bce5b6d | ||
|
|
88f1041f89 | ||
|
|
c15bb73d45 | ||
|
|
023eb8c426 | ||
|
|
9ffa3ee8f3 | ||
|
|
a8147a1697 | ||
|
|
b2190b7b9a | ||
|
|
f39b98e4d1 | ||
|
|
2d81b497cf | ||
|
|
d100c7d95d | ||
|
|
03d406021f | ||
|
|
844d81072f | ||
|
|
1d7149a26d | ||
|
|
2434f3465b | ||
|
|
0dbd8649cb |
@@ -1,9 +1,13 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:mattermost/react"
|
||||
"plugin:mattermost/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"mattermost"
|
||||
"mattermost",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
@@ -20,7 +24,16 @@
|
||||
"rules": {
|
||||
"global-require": 0,
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-filename-extension": [2, {"extensions": [".js"]}]
|
||||
"react/jsx-filename-extension": [2, {"extensions": [".js"]}],
|
||||
"no-undefined": 0,
|
||||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/no-undefined": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-unused-vars": 2,
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -87,6 +87,7 @@ ios/sentry.properties
|
||||
# Testing
|
||||
.nyc_output
|
||||
coverage
|
||||
.tmp
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
380
NOTICE.txt
380
NOTICE.txt
@@ -876,223 +876,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## mattermost-redux
|
||||
|
||||
This product contains 'mattermost-redux' by Mattermost.
|
||||
|
||||
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/mattermost-redux
|
||||
|
||||
* LICENSE: Apache-2.0
|
||||
|
||||
Copyright 2015-present Mattermost, Inc.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## mime-db
|
||||
|
||||
This product contains 'mime-db' by Douglas Christopher Wilson.
|
||||
@@ -1988,6 +1771,41 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE
|
||||
|
||||
---
|
||||
|
||||
## react-native-mmkv-storage
|
||||
|
||||
This product contains 'react-native-mmkv-storage' by Ammar Ahmed.
|
||||
|
||||
An efficient, small & encrypted mobile key-value storage framework for React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/ammarahm-ed/react-native-mmkv-storage
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Ammar Ahmed
|
||||
|
||||
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-navigation
|
||||
|
||||
This product contains a modified version of 'react-native-navigation' by Wix.com.
|
||||
@@ -2592,6 +2410,41 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## redux-action-buffer
|
||||
|
||||
This product contains 'redux-action-buffer' by Zack Story.
|
||||
|
||||
A middleware for redux that buffers all actions into a queue until a breaker condition is met, at which point the queue is released (i.e. actions are triggered).
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/rt2zz/redux-action-buffer
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 rt2zz and 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.
|
||||
|
||||
---
|
||||
|
||||
## redux-batched-actions
|
||||
|
||||
This product contains 'redux-batched-actions' by Tim Shelburne.
|
||||
@@ -2627,41 +2480,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## redux-offline
|
||||
|
||||
This product contains 'redux-offline' by redux-offline.
|
||||
|
||||
Build Offline-First Apps for Web and React Native
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/redux-offline/redux-offline
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Jani Eväkallio
|
||||
|
||||
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-persist
|
||||
|
||||
This product contains 'redux-persist' by Zack Story.
|
||||
@@ -2732,6 +2550,41 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## redux-reset
|
||||
|
||||
This product contains 'redux-reset' by Wang Zixiao.
|
||||
|
||||
Gives redux the ability to reset the state
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/wwayne/redux-reset
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Wang Zixiao
|
||||
|
||||
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-thunk
|
||||
|
||||
This product contains 'redux-thunk' by Dan Abramov.
|
||||
@@ -2900,6 +2753,29 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## serialize-error
|
||||
|
||||
This product contains 'serialize-error' by Sindre Sorhus.
|
||||
|
||||
Serialize/deserialize an error into a plain object
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/sindresorhus/serialize-error
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## shallow-equals
|
||||
|
||||
This product contains 'shallow-equals' by Hugh Kennedy.
|
||||
|
||||
@@ -24,7 +24,6 @@ Otherwise, link the JIRA ticket.
|
||||
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
|
||||
-->
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] All new/modified APIs include changes to [mattermost-redux](https://github.com/mattermost/mattermost-redux) (please link)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.19)
|
||||
- **Supported iOS versions:** 10.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).
|
||||
|
||||
@@ -130,8 +130,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 285
|
||||
versionName "1.30.0"
|
||||
versionCode 296
|
||||
versionName "1.31.2"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {networkStatusChangedAction} from 'redux-offline';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
export function connection(isOnline) {
|
||||
return async (dispatch) => {
|
||||
dispatch(networkStatusChangedAction(isOnline));
|
||||
dispatch({
|
||||
type: DeviceTypes.CONNECTION_CHANGED,
|
||||
data: isOnline,
|
||||
});
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (isOnline !== undefined && isOnline !== state.device.connection) { //eslint-disable-line no-undefined
|
||||
dispatch({
|
||||
type: DeviceTypes.CONNECTION_CHANGED,
|
||||
data: isOnline,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
355
app/actions/helpers/channels.ts
Normal file
355
app/actions/helpers/channels.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getMyPreferences} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
|
||||
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
|
||||
|
||||
import {ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
|
||||
import {PreferenceType} from '@mm-redux/types/preferences';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {RelationOneToMany} from '@mm-redux/types/utilities';
|
||||
|
||||
import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels';
|
||||
import {buildPreference} from '@utils/preferences';
|
||||
|
||||
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: Array<PreferenceType> = [];
|
||||
const promises: Array<Promise<ActionResult>> = []; //only fetch profiles that we don't have and the Direct channel should be visible
|
||||
const actions = [];
|
||||
|
||||
// Prepare preferences and start fetching profiles to batch them
|
||||
directChannels.forEach((c) => {
|
||||
const profileIds = Array.from(usersInChannel[c.id] || []);
|
||||
const profilesInChannel: Array<string> = profileIds.filter((u: string) => u !== currentUserId);
|
||||
switch (c.type) {
|
||||
case General.DM_CHANNEL: {
|
||||
const dm = fetchDirectMessageProfileIfNeeded(state, c, channelMembers, profilesInChannel);
|
||||
if (dm.preferences.length) {
|
||||
prefs.push(...dm.preferences);
|
||||
}
|
||||
|
||||
if (dm.promise) {
|
||||
promises.push(dm.promise);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case General.GM_CHANNEL: {
|
||||
const gm = fetchGroupMessageProfilesIfNeeded(state, c, channelMembers, profilesInChannel);
|
||||
|
||||
if (gm.preferences.length) {
|
||||
prefs.push(...gm.preferences);
|
||||
}
|
||||
|
||||
if (gm.promise) {
|
||||
promises.push(gm.promise);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save preferences if there are any changes
|
||||
if (prefs.length) {
|
||||
Client4.savePreferences(currentUserId, prefs);
|
||||
actions.push({
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: prefs,
|
||||
});
|
||||
}
|
||||
|
||||
const profilesAction = await getProfilesFromPromises(promises);
|
||||
if (profilesAction) {
|
||||
actions.push(profilesAction);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function fetchMyChannel(channelId: string) {
|
||||
try {
|
||||
const data = await Client4.getChannel(channelId);
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMyChannelMember(channelId: string) {
|
||||
try {
|
||||
const data = await Client4.getMyChannelMember(channelId);
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>): Array<GenericAction> {
|
||||
const {myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
const actions: GenericAction[] = [{
|
||||
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
amount: 1,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
teamId,
|
||||
channelId,
|
||||
amount: 1,
|
||||
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
|
||||
myMembers[channelId].notify_props.mark_unread === General.MENTION,
|
||||
},
|
||||
}];
|
||||
|
||||
if (mentions && mentions.indexOf(currentUserId) !== -1) {
|
||||
actions.push({
|
||||
type: ChannelTypes.INCREMENT_UNREAD_MENTION_COUNT,
|
||||
data: {
|
||||
teamId,
|
||||
channelId,
|
||||
amount: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export function makeDirectChannelVisibleIfNecessary(state: GlobalState, otherUserId: string): GenericAction|null {
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId)];
|
||||
|
||||
if (!preference || preference.value === 'false') {
|
||||
preference = {
|
||||
user_id: currentUserId,
|
||||
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
name: otherUserId,
|
||||
value: 'true',
|
||||
};
|
||||
|
||||
return {
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: [preference],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function makeGroupMessageVisibleIfNecessary(state: GlobalState, channelId: string) {
|
||||
try {
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
let preference = myPreferences[getPreferenceKey(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, channelId)];
|
||||
|
||||
if (!preference || preference.value === 'false') {
|
||||
preference = {
|
||||
user_id: currentUserId,
|
||||
category: Preferences.CATEGORY_GROUP_CHANNEL_SHOW,
|
||||
name: channelId,
|
||||
value: 'true',
|
||||
};
|
||||
|
||||
const profilesInChannel = await fetchUsersInChannel(state, channelId);
|
||||
|
||||
return [{
|
||||
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
|
||||
data: [profilesInChannel],
|
||||
}, {
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: [preference],
|
||||
}];
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelAndMyMember(channelId: string): Promise<Array<GenericAction>> {
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
try {
|
||||
const [channel, member] = await Promise.all([
|
||||
Client4.getChannel(channelId),
|
||||
Client4.getMyChannelMember(channelId),
|
||||
]);
|
||||
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_CHANNEL,
|
||||
data: channel,
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
|
||||
data: member,
|
||||
});
|
||||
|
||||
const roles = await Client4.getRolesByNames(member.roles.split(' '));
|
||||
if (roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: roles,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
export async function getAddedDmUsersIfNecessary(state: GlobalState, preferences: PreferenceType[]): Promise<Array<GenericAction>> {
|
||||
const userIds: string[] = [];
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
for (const preference of preferences) {
|
||||
if (preference.category === Preferences.CATEGORY_DIRECT_CHANNEL_SHOW && preference.value === 'true') {
|
||||
userIds.push(preference.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.length !== 0) {
|
||||
const profiles = getUsers(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
const needProfiles: string[] = [];
|
||||
|
||||
for (const userId of userIds) {
|
||||
if (!profiles[userId] && userId !== currentUserId) {
|
||||
needProfiles.push(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (needProfiles.length > 0) {
|
||||
const data = await Client4.getProfilesByIds(userIds);
|
||||
if (profiles.lenght) {
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel, channelMembers: Array<ChannelMembership>, profilesInChannel: Array<string>) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const users = getUsers(state);
|
||||
const config = getConfig(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const otherUser = users[otherUserId];
|
||||
const dmVisible = isDirectChannelVisible(currentUserId, myPreferences, channel);
|
||||
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);
|
||||
const preferences = [];
|
||||
|
||||
// when then DM is hidden but has new messages
|
||||
if ((!dmVisible || dmAutoClosed) && dmIsUnread) {
|
||||
preferences.push(buildPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, currentUserId, otherUserId));
|
||||
preferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (dmFetchProfile && !profilesInChannel.includes(otherUserId) && otherUserId !== currentUserId) {
|
||||
return {
|
||||
preferences,
|
||||
promise: fetchUsersInChannel(state, channel.id),
|
||||
};
|
||||
}
|
||||
|
||||
return {preferences};
|
||||
}
|
||||
|
||||
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.id);
|
||||
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;
|
||||
|
||||
if (channelMember) {
|
||||
hasMentions = channelMember.mention_count > 0;
|
||||
isUnread = channelMember.msg_count < channel.total_msg_count;
|
||||
}
|
||||
|
||||
const gmIsUnread = hasMentions || isUnread;
|
||||
const gmFetchProfile = gmIsUnread || (gmVisible && !gmAutoClosed);
|
||||
const preferences = [];
|
||||
|
||||
// when then GM is hidden but has new messages
|
||||
if ((!gmVisible || gmAutoClosed) && gmIsUnread) {
|
||||
preferences.push(buildPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, currentUserId, channel.id));
|
||||
preferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (gmFetchProfile && !profilesInChannel.length) {
|
||||
return {
|
||||
preferences,
|
||||
promise: fetchUsersInChannel(state, channel.id),
|
||||
};
|
||||
}
|
||||
|
||||
return {preferences};
|
||||
}
|
||||
|
||||
async function fetchUsersInChannel(state: GlobalState, channelId: string): Promise<ActionResult> {
|
||||
try {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const profiles = await Client4.getProfilesInChannel(channelId);
|
||||
|
||||
// When fetching profiles in channels we exclude our own user
|
||||
const users = profiles.filter((p: UserProfile) => p.id !== currentUserId);
|
||||
const data = {
|
||||
channelId,
|
||||
users,
|
||||
};
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
async function getProfilesFromPromises(promises: Array<Promise<ActionResult>>): Promise<GenericAction | null> {
|
||||
// Get the profiles returned by the promises
|
||||
if (!promises.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await Promise.all(promises);
|
||||
const data = result.filter((p: any) => !p.error);
|
||||
|
||||
return {
|
||||
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -5,15 +5,15 @@ import {Keyboard, Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import store from 'app/store';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Store from '@store/store';
|
||||
|
||||
const CHANNEL_SCREEN = 'Channel';
|
||||
|
||||
function getThemeFromState() {
|
||||
const state = store.getState();
|
||||
const state = Store.redux?.getState() || {};
|
||||
|
||||
return getTheme(state);
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export function resetToChannel(passProps = {}) {
|
||||
},
|
||||
backButton: {
|
||||
visible: false,
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,21 +3,27 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import * as NavigationActions from '@actions/navigation';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import intitialState from '@store/initial_state';
|
||||
import Store from '@store/store';
|
||||
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import * as NavigationActions from 'app/actions/navigation';
|
||||
|
||||
jest.unmock('app/actions/navigation');
|
||||
jest.mock('app/store/ephemeral_store', () => ({
|
||||
jest.unmock('@actions/navigation');
|
||||
jest.mock('@store/ephemeral_store', () => ({
|
||||
getNavigationTopComponentId: jest.fn(),
|
||||
clearNavigationComponents: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('app/actions/navigation', () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const store = mockStore(intitialState);
|
||||
Store.redux = store;
|
||||
|
||||
describe('@actions/navigation', () => {
|
||||
const topComponentId = 'top-component-id';
|
||||
const name = 'name';
|
||||
const title = 'title';
|
||||
@@ -53,6 +59,7 @@ describe('app/actions/navigation', () => {
|
||||
height: 0,
|
||||
backButton: {
|
||||
visible: false,
|
||||
color: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
background: {
|
||||
color: theme.sidebarHeaderBg,
|
||||
|
||||
@@ -5,54 +5,37 @@ import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {UserTypes, ChannelTypes} from 'mattermost-redux/action_types';
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByNameAndTeamName,
|
||||
markChannelAsRead,
|
||||
markChannelAsViewed,
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from 'mattermost-redux/actions/channels';
|
||||
import {getFilesForPost} from 'mattermost-redux/actions/files';
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
import {getTeamMembersByIds, selectTeam} from 'mattermost-redux/actions/teams';
|
||||
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General, Preferences} from 'mattermost-redux/constants';
|
||||
import {getPostIdsInChannel} from 'mattermost-redux/selectors/entities/posts';
|
||||
} from '@mm-redux/actions/channels';
|
||||
import {getFilesForPost} from '@mm-redux/actions/files';
|
||||
import {savePreferences} from '@mm-redux/actions/preferences';
|
||||
import {selectTeam} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
getChannelsNameMapInTeam,
|
||||
isManuallyUnread,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUserIdsInChannels, getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {
|
||||
getChannelByName,
|
||||
getDirectChannelName,
|
||||
getUserIdFromChannelName,
|
||||
isDirectChannel,
|
||||
isGroupChannel,
|
||||
getChannelByName as getChannelByNameSelector,
|
||||
} from 'mattermost-redux/utils/channel_utils';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
|
||||
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
|
||||
import {getChannelByName as selectChannelByName} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox';
|
||||
import {getChannelReachable} from 'app/selectors/channel';
|
||||
import telemetry from 'app/telemetry';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, isDirectMessageVisible, isGroupMessageVisible, isDirectChannelAutoClosed} from 'app/utils/channels';
|
||||
import {isPendingPost} from 'app/utils/general';
|
||||
import {buildPreference} from 'app/utils/preferences';
|
||||
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from './post';
|
||||
import {forceLogoutIfNecessary} from './user';
|
||||
import {loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, getPostThread, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_textbox';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import telemetry from '@telemetry';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue} from '@utils/channels';
|
||||
import {isPendingPost} from '@utils/general';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
@@ -72,146 +55,33 @@ export function loadChannelsByTeamName(teamName, errorHandler) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId, profilesInChannel} = state.entities.users;
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const {myPreferences} = state.entities.preferences;
|
||||
const {membersInTeam} = state.entities.teams;
|
||||
const dmPrefs = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
|
||||
const gmPrefs = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_GROUP_CHANNEL_SHOW);
|
||||
const members = [];
|
||||
const loadProfilesForChannels = [];
|
||||
const prefs = [];
|
||||
|
||||
function buildPref(name) {
|
||||
return {
|
||||
user_id: currentUserId,
|
||||
category: Preferences.CATEGORY_DIRECT_CHANNEL_SHOW,
|
||||
name,
|
||||
value: 'true',
|
||||
};
|
||||
}
|
||||
|
||||
// Find DM's and GM's that need to be shown
|
||||
const directChannels = Object.values(channels).filter((c) => (isDirectChannel(c) || isGroupChannel(c)));
|
||||
directChannels.forEach((channel) => {
|
||||
const member = myMembers[channel.id];
|
||||
if (isDirectChannel(channel) && !isDirectChannelVisible(currentUserId, myPreferences, channel) && member && member.mention_count > 0) {
|
||||
const teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
let pref = dmPrefs.get(teammateId);
|
||||
if (pref) {
|
||||
pref = {...pref, value: 'true'};
|
||||
} else {
|
||||
pref = buildPref(teammateId);
|
||||
}
|
||||
dmPrefs.set(teammateId, pref);
|
||||
prefs.push(pref);
|
||||
} else if (isGroupChannel(channel) && !isGroupChannelVisible(myPreferences, channel) && member && (member.mention_count > 0 || member.msg_count < channel.total_msg_count)) {
|
||||
const id = channel.id;
|
||||
let pref = gmPrefs.get(id);
|
||||
if (pref) {
|
||||
pref = {...pref, value: 'true'};
|
||||
} else {
|
||||
pref = buildPref(id);
|
||||
}
|
||||
gmPrefs.set(id, pref);
|
||||
prefs.push(pref);
|
||||
}
|
||||
});
|
||||
|
||||
if (prefs.length) {
|
||||
savePreferences(currentUserId, prefs)(dispatch, getState);
|
||||
}
|
||||
|
||||
for (const [key, pref] of dmPrefs) {
|
||||
if (pref.value === 'true') {
|
||||
members.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, pref] of gmPrefs) {
|
||||
//only load the profiles in channels if we don't already have them
|
||||
if (pref.value === 'true' && !profilesInChannel[key]) {
|
||||
loadProfilesForChannels.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadProfilesForChannels.length) {
|
||||
for (let i = 0; i < loadProfilesForChannels.length; i++) {
|
||||
const channelId = loadProfilesForChannels[i];
|
||||
getProfilesInChannel(channelId, 0)(dispatch, getState);
|
||||
}
|
||||
}
|
||||
|
||||
let membersToLoad = members;
|
||||
if (membersInTeam[teamId]) {
|
||||
membersToLoad = members.filter((m) => !membersInTeam[teamId].hasOwnProperty(m));
|
||||
}
|
||||
|
||||
if (membersToLoad.length) {
|
||||
getTeamMembersByIds(teamId, membersToLoad)(dispatch, getState);
|
||||
}
|
||||
|
||||
const actions = [];
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const channelName = getDirectChannelName(currentUserId, members[i]);
|
||||
const channel = getChannelByName(channels, channelName);
|
||||
if (channel) {
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_PROFILE_IN_CHANNEL,
|
||||
data: {id: channel.id, user_id: members[i]},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {posts} = state.entities.posts;
|
||||
const postsIds = getPostIdsInChannel(state, channelId);
|
||||
const postIds = getPostIdsInChannel(state, channelId);
|
||||
const actions = [];
|
||||
|
||||
const time = Date.now();
|
||||
|
||||
let loadMorePostsVisible = true;
|
||||
let postAction;
|
||||
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
if (!postIds || postIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
postAction = getPosts(channelId);
|
||||
} else {
|
||||
const lastConnectAt = state.websocket?.lastConnectAt || 0;
|
||||
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
|
||||
|
||||
let since;
|
||||
if (lastGetPosts && lastGetPosts < lastConnectAt) {
|
||||
// Since the websocket disconnected, we may have missed some posts since then
|
||||
since = lastGetPosts;
|
||||
} else {
|
||||
// Trust that we've received all posts since the last time the websocket disconnected
|
||||
// so just get any that have changed since the latest one we've received
|
||||
const postsForChannel = postsIds.map((id) => posts[id]);
|
||||
since = getLastCreateAt(postsForChannel);
|
||||
}
|
||||
|
||||
const since = getChannelSinceValue(state, channelId, postIds);
|
||||
postAction = getPostsSince(channelId, since);
|
||||
}
|
||||
|
||||
const received = await retryGetPostsAction(postAction, dispatch, getState);
|
||||
const received = await dispatch(fetchPostActionWithRetry(postAction));
|
||||
|
||||
if (received) {
|
||||
actions.push({
|
||||
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
||||
channelId,
|
||||
time,
|
||||
});
|
||||
},
|
||||
setChannelRetryFailed(false));
|
||||
|
||||
if (received?.order) {
|
||||
const count = received.order.length;
|
||||
@@ -220,22 +90,24 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
|
||||
}
|
||||
|
||||
actions.push(setLoadMorePostsVisible(loadMorePostsVisible));
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_POSTS_IN_CHANNEL'));
|
||||
};
|
||||
}
|
||||
|
||||
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_RETRIES) {
|
||||
for (let i = 0; i <= maxTries; i++) {
|
||||
const {data} = await dispatch(action); // eslint-disable-line no-await-in-loop
|
||||
export function fetchPostActionWithRetry(action, maxTries = MAX_RETRIES) {
|
||||
return async (dispatch) => {
|
||||
for (let i = 0; i <= maxTries; i++) {
|
||||
const {data} = await dispatch(action); // eslint-disable-line no-await-in-loop
|
||||
|
||||
if (data) {
|
||||
dispatch(setChannelRetryFailed(false));
|
||||
return data;
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setChannelRetryFailed(true));
|
||||
return null;
|
||||
dispatch(setChannelRetryFailed(true));
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFilesForPostIfNecessary(postId) {
|
||||
@@ -325,7 +197,7 @@ export function selectDefaultChannel(teamId) {
|
||||
const state = getState();
|
||||
|
||||
const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
|
||||
const channel = getChannelByNameSelector(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
const channel = selectChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
|
||||
let channelId;
|
||||
if (channel) {
|
||||
channelId = channel.id;
|
||||
@@ -365,7 +237,7 @@ export function handleSelectChannel(channelId) {
|
||||
teamId: channel.team_id || currentTeamId,
|
||||
},
|
||||
});
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
|
||||
}
|
||||
|
||||
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
@@ -406,10 +278,16 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
}
|
||||
|
||||
export function handlePostDraftChanged(channelId, draft) {
|
||||
return {
|
||||
type: ViewTypes.POST_DRAFT_CHANGED,
|
||||
channelId,
|
||||
draft,
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.views.channel.drafts[channelId]?.draft !== draft) {
|
||||
dispatch({
|
||||
type: ViewTypes.POST_DRAFT_CHANGED,
|
||||
channelId,
|
||||
draft,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -425,13 +303,15 @@ export function insertToDraft(value) {
|
||||
}
|
||||
|
||||
export function markChannelViewedAndRead(channelId, previousChannelId, markOnServer = true) {
|
||||
return (dispatch) => {
|
||||
dispatch(markChannelAsRead(channelId, previousChannelId, markOnServer));
|
||||
dispatch(markChannelAsViewed(channelId, previousChannelId));
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const actions = markAsViewedAndReadBatch(state, channelId, previousChannelId, markOnServer);
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_MARK_CHANNEL_VIEWED_AND_READ'));
|
||||
};
|
||||
}
|
||||
|
||||
function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', markOnServer = true) {
|
||||
export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', markOnServer = true) {
|
||||
const actions = [];
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const channel = channels[channelId];
|
||||
@@ -512,8 +392,7 @@ export function markChannelViewedAndReadOnReconnect(channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(markChannelAsRead(channelId));
|
||||
dispatch(markChannelAsViewed(channelId));
|
||||
dispatch(markChannelViewedAndRead(channelId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -579,10 +458,16 @@ export function closeGMChannel(channel) {
|
||||
}
|
||||
|
||||
export function refreshChannelWithRetry(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
return async (dispatch) => {
|
||||
dispatch(setChannelRefreshing(true));
|
||||
const posts = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
|
||||
dispatch(setChannelRefreshing(false));
|
||||
const posts = await dispatch(fetchPostActionWithRetry(getPosts(channelId)));
|
||||
const actions = [setChannelRefreshing(false)];
|
||||
|
||||
if (posts) {
|
||||
actions.push(setChannelRetryFailed(false));
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_REEFRESH_CHANNEL'));
|
||||
return posts;
|
||||
};
|
||||
}
|
||||
@@ -675,7 +560,8 @@ export function increasePostVisibility(channelId, postId) {
|
||||
|
||||
const pageSize = ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
|
||||
|
||||
const result = await retryGetPostsAction(getPostsBefore(channelId, postId, 0, pageSize), dispatch, getState);
|
||||
const postAction = getPostsBefore(channelId, postId, 0, pageSize);
|
||||
const result = await dispatch(fetchPostActionWithRetry(postAction));
|
||||
|
||||
const actions = [{
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
@@ -683,6 +569,10 @@ export function increasePostVisibility(channelId, postId) {
|
||||
channelId,
|
||||
}];
|
||||
|
||||
if (result) {
|
||||
actions.push(setChannelRetryFailed(false));
|
||||
}
|
||||
|
||||
let hasMorePost = false;
|
||||
if (result?.order) {
|
||||
const count = result.order.length;
|
||||
@@ -691,7 +581,7 @@ export function increasePostVisibility(channelId, postId) {
|
||||
actions.push(setLoadMorePostsVisible(hasMorePost));
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_MORE_POSTS'));
|
||||
telemetry.end(['posts:loading']);
|
||||
telemetry.save();
|
||||
|
||||
@@ -706,13 +596,14 @@ function setLoadMorePostsVisible(visible) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadChannelsForTeam(teamId) {
|
||||
export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const data = {sync: true, teamId};
|
||||
const actions = [];
|
||||
|
||||
if (currentUserId) {
|
||||
const data = {sync: true, teamId};
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
try {
|
||||
console.log('Fetching channels attempt', teamId, (i + 1)); //eslint-disable-line no-console
|
||||
@@ -725,8 +616,7 @@ export function loadChannelsForTeam(teamId) {
|
||||
data.channelMembers = channelMembers;
|
||||
break;
|
||||
} catch (err) {
|
||||
const result = await dispatch(forceLogoutIfNecessary(err)); //eslint-disable-line no-await-in-loop
|
||||
if (result || i === MAX_RETRIES) {
|
||||
if (i === MAX_RETRIES) {
|
||||
const hasChannelsLoaded = state.entities.channels.channelsInTeam[teamId]?.size > 0;
|
||||
return {error: hasChannelsLoaded ? null : err};
|
||||
}
|
||||
@@ -734,189 +624,52 @@ export function loadChannelsForTeam(teamId) {
|
||||
}
|
||||
|
||||
if (data.channels) {
|
||||
const roles = new Set();
|
||||
const members = data.channelMembers;
|
||||
for (const member of members) {
|
||||
for (const role of member.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (roles.size > 0) {
|
||||
dispatch(loadRolesIfNeeded(roles));
|
||||
}
|
||||
|
||||
// Fetch needed profiles from channel creators and direct channels
|
||||
dispatch(loadSidebarDirectMessagesProfiles(data));
|
||||
|
||||
dispatch({
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
return {data};
|
||||
if (!skipDispatch) {
|
||||
const rolesToLoad = new Set();
|
||||
const members = data.channelMembers;
|
||||
for (const member of members) {
|
||||
for (const role of member.roles.split(' ')) {
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (rolesToLoad.size > 0) {
|
||||
data.roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
|
||||
if (data.roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CHANNELS_FOR_TEAM'));
|
||||
}
|
||||
|
||||
// Fetch needed profiles from channel creators and direct channels
|
||||
dispatch(loadSidebar(data));
|
||||
|
||||
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
||||
}
|
||||
}
|
||||
|
||||
return {error: 'Cannot fetch channels without a current user'};
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
export function loadSidebarDirectMessagesProfiles(data) {
|
||||
function loadSidebar(data) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {channels, channelMembers} = data;
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const usersInChannel = getUserIdsInChannels(state);
|
||||
const directChannels = Object.values(channels).filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
|
||||
const prefs = [];
|
||||
const promises = []; //only fetch profiles that we don't have and the Direct channel should be visible
|
||||
const userIds = [];
|
||||
|
||||
// Prepare preferences and start fetching profiles to batch them
|
||||
directChannels.forEach((c) => {
|
||||
const profilesInChannel = Array.from(usersInChannel[c.id] || []).filter((u) => u.id !== currentUserId);
|
||||
userIds.push(...profilesInChannel);
|
||||
|
||||
switch (c.type) {
|
||||
case General.DM_CHANNEL: {
|
||||
const dm = fetchDirectMessageProfileIfNeeded(state, c, channelMembers, profilesInChannel, prefs);
|
||||
|
||||
if (dm) {
|
||||
promises.push(dispatch(dm));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case General.GM_CHANNEL: {
|
||||
const gm = fetchGroupMessageProfilesIfNeeded(state, c, channelMembers, profilesInChannel, prefs);
|
||||
|
||||
if (gm) {
|
||||
promises.push(dispatch(gm));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Save preferences if there are any changes
|
||||
if (prefs.length) {
|
||||
dispatch(savePreferences(currentUserId, prefs));
|
||||
}
|
||||
|
||||
const actions = [];
|
||||
const userIdsSet = new Set(userIds);
|
||||
const profilesAction = await getProfilesFromPromises(promises);
|
||||
if (profilesAction) {
|
||||
actions.push(profilesAction);
|
||||
profilesAction.data.forEach((d) => {
|
||||
const {users} = d.data;
|
||||
users.forEach((u) => userIdsSet.add(u.id));
|
||||
});
|
||||
}
|
||||
|
||||
if (userIdsSet.size > 0) {
|
||||
try {
|
||||
const statuses = await Client4.getStatusesByIds(Array.from(userIdsSet));
|
||||
if (statuses.length) {
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: statuses,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// do nothing (status will get fetched later on regardless)
|
||||
}
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function getUsersInChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const profiles = await Client4.getProfilesInChannel(channelId);
|
||||
|
||||
// When fetching profiles in channels we exclude our own user
|
||||
const users = profiles.filter((p) => p.id !== currentUserId);
|
||||
const data = {
|
||||
channelId,
|
||||
users,
|
||||
};
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
const sidebarActions = await loadSidebarDirectMessagesProfiles(state, channels, channelMembers);
|
||||
if (sidebarActions.length) {
|
||||
dispatch(batchActions(sidebarActions, 'BATCH_LOAD_SIDEBAR'));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getProfilesFromPromises(promises) {
|
||||
// Get the profiles returned by the promises
|
||||
if (!promises.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await Promise.all(promises);
|
||||
const data = result.filter((p) => !p.error);
|
||||
|
||||
return {
|
||||
type: UserTypes.RECEIVED_BATCHED_PROFILES_IN_CHANNEL,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
function fetchDirectMessageProfileIfNeeded(state, channel, channelMembers, profilesInChannel, newPreferences) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const preferences = getMyPreferences(state);
|
||||
const users = getUsers(state);
|
||||
const config = getConfig(state);
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const otherUser = users[otherUserId];
|
||||
const dmVisible = isDirectMessageVisible(preferences, channel.id);
|
||||
const dmAutoClosed = isDirectChannelAutoClosed(config, preferences, channel.id, channel.last_post_at, otherUser?.delete_at, currentChannelId); //eslint-disable-line camelcase
|
||||
const dmIsUnread = channelMembers[channel.id]?.mention_count > 0; //eslint-disable-line camelcase
|
||||
const dmFetchProfile = dmIsUnread || (dmVisible && !dmAutoClosed);
|
||||
|
||||
// when then DM is hidden but has new messages
|
||||
if ((!dmVisible || dmAutoClosed) && dmIsUnread) {
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, currentUserId, otherUserId));
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (dmFetchProfile && !profilesInChannel.includes(otherUserId) && otherUserId !== currentUserId) {
|
||||
return getUsersInChannel(channel.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fetchGroupMessageProfilesIfNeeded(state, channel, channelMembers, profilesInChannel, newPreferences) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const preferences = getMyPreferences(state);
|
||||
const config = getConfig(state);
|
||||
const gmVisible = isGroupMessageVisible(preferences, channel.id);
|
||||
const gmAutoClosed = isDirectChannelAutoClosed(config, preferences, channel.id, channel.last_post_at);
|
||||
const channelMember = channelMembers[channel.id];
|
||||
const gmIsUnread = channelMember?.mention_count > 0 || channelMember?.msg_count < channel.total_msg_count; //eslint-disable-line camelcase
|
||||
const gmFetchProfile = gmIsUnread || (gmVisible && !gmAutoClosed);
|
||||
|
||||
// when then GM is hidden but has new messages
|
||||
if ((!gmVisible || gmAutoClosed) && gmIsUnread) {
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_GROUP_CHANNEL_SHOW, currentUserId, channel.id));
|
||||
newPreferences.push(buildPreference(Preferences.CATEGORY_CHANNEL_OPEN_TIME, currentUserId, channel.id, Date.now().toString()));
|
||||
}
|
||||
|
||||
if (gmFetchProfile && !profilesInChannel.length) {
|
||||
return getUsersInChannel(channel.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,24 +4,25 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import initialState from 'app/initial_state';
|
||||
import {ChannelTypes} from 'mattermost-redux/action_types';
|
||||
import testHelper from 'test/test_helper';
|
||||
|
||||
import * as ChannelActions from 'app/actions/views/channel';
|
||||
import * as ChannelActions from '@actions/views/channel';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import postReducer from '@mm-redux/reducers/entities/posts';
|
||||
import initialState from '@store/initial_state';
|
||||
|
||||
const {
|
||||
handleSelectChannel,
|
||||
handleSelectChannelByName,
|
||||
loadPostsIfNecessaryWithRetry,
|
||||
} = ChannelActions;
|
||||
|
||||
import postReducer from 'mattermost-redux/reducers/entities/posts';
|
||||
|
||||
const MOCK_CHANNEL_MARK_AS_READ = 'MOCK_CHANNEL_MARK_AS_READ';
|
||||
const MOCK_CHANNEL_MARK_AS_VIEWED = 'MOCK_CHANNEL_MARK_AS_VIEWED';
|
||||
|
||||
jest.mock('mattermost-redux/actions/channels', () => {
|
||||
const channelActions = require.requireActual('mattermost-redux/actions/channels');
|
||||
jest.mock('@mm-redux/actions/channels', () => {
|
||||
const channelActions = require.requireActual('@mm-redux/actions/channels');
|
||||
return {
|
||||
...channelActions,
|
||||
markChannelAsRead: jest.fn().mockReturnValue({type: 'MOCK_CHANNEL_MARK_AS_READ'}),
|
||||
@@ -29,8 +30,8 @@ jest.mock('mattermost-redux/actions/channels', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/teams', () => {
|
||||
const teamSelectors = require.requireActual('mattermost-redux/selectors/entities/teams');
|
||||
jest.mock('@mm-redux/selectors/entities/teams', () => {
|
||||
const teamSelectors = require.requireActual('@mm-redux/selectors/entities/teams');
|
||||
return {
|
||||
...teamSelectors,
|
||||
getTeamByName: jest.fn(() => ({name: 'current-team-name'})),
|
||||
@@ -48,7 +49,7 @@ describe('Actions.Views.Channel', () => {
|
||||
const MOCK_RECEIVED_POSTS_IN_CHANNEL = 'RECEIVED_POSTS_IN_CHANNEL';
|
||||
const MOCK_RECEIVED_POSTS_SINCE = 'MOCK_RECEIVED_POSTS_SINCE';
|
||||
|
||||
const actions = require('mattermost-redux/actions/channels');
|
||||
const actions = require('@mm-redux/actions/channels');
|
||||
actions.getChannelByNameAndTeamName = jest.fn((teamName) => {
|
||||
if (teamName) {
|
||||
return {
|
||||
@@ -96,7 +97,7 @@ describe('Actions.Views.Channel', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const postUtils = require('mattermost-redux/utils/post_utils');
|
||||
const postUtils = require('@mm-redux/utils/post_utils');
|
||||
postUtils.getLastCreateAt = jest.fn((array) => {
|
||||
return array[0].create_at;
|
||||
});
|
||||
@@ -138,7 +139,7 @@ describe('Actions.Views.Channel', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const channelSelectors = require('mattermost-redux/selectors/entities/channels');
|
||||
const channelSelectors = require('@mm-redux/selectors/entities/channels');
|
||||
channelSelectors.getChannel = jest.fn((state, channelId) => ({data: channelId}));
|
||||
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
|
||||
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
|
||||
@@ -211,9 +212,9 @@ describe('Actions.Views.Channel', () => {
|
||||
expect(postActions.getPosts).toBeCalled();
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const storeBatchActions = storeActions.filter(({type}) => type === 'BATCH_LOAD_POSTS_IN_CHANNEL');
|
||||
const receivedPosts = storeActions.find(({type}) => type === MOCK_RECEIVED_POSTS);
|
||||
const receivedPostsAtAction = storeBatchActions[0].payload.some((action) => action.type === 'RECEIVED_POSTS_FOR_CHANNEL_AT_TIME');
|
||||
const receivedPostsAtAction = storeBatchActions[0].payload.some((action) => action.type === ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME);
|
||||
|
||||
nextPostState = postReducer(store.getState().entities.posts, receivedPosts);
|
||||
nextPostState = postReducer(nextPostState, {
|
||||
@@ -297,7 +298,7 @@ describe('Actions.Views.Channel', () => {
|
||||
|
||||
await store.dispatch(handleSelectChannel(channelId));
|
||||
const storeActions = store.getActions();
|
||||
const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH');
|
||||
const storeBatchActions = storeActions.find(({type}) => type === 'BATCH_SWITCH_CHANNEL');
|
||||
const selectChannelWithMember = storeBatchActions?.payload.find(({type}) => type === ChannelTypes.SELECT_CHANNEL);
|
||||
const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED);
|
||||
const readAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_READ);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {addChannelMember} from 'mattermost-redux/actions/channels';
|
||||
import {addChannelMember} from '@mm-redux/actions/channels';
|
||||
|
||||
export function handleAddChannelMembers(channelId, members) {
|
||||
return async (dispatch) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {removeChannelMember} from 'mattermost-redux/actions/channels';
|
||||
import {removeChannelMember} from '@mm-redux/actions/channels';
|
||||
|
||||
export function handleRemoveChannelMembers(channelId, members) {
|
||||
return async (dispatch, getState) => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
export function executeCommand(message, channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {handleSelectChannel, setChannelDisplayName} from './channel';
|
||||
import {createChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {cleanUpUrlable} from 'mattermost-redux/utils/channel_utils';
|
||||
import {generateId} from 'mattermost-redux/utils/helpers';
|
||||
import {createChannel} from '@mm-redux/actions/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {cleanUpUrlable} from '@mm-redux/utils/channel_utils';
|
||||
import {generateId} from '@mm-redux/utils/helpers';
|
||||
|
||||
export function generateChannelNameFromDisplayName(displayName) {
|
||||
let name = cleanUpUrlable(displayName);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {updateMe, setDefaultProfileImage} from 'mattermost-redux/actions/users';
|
||||
import {updateMe, setDefaultProfileImage} from '@mm-redux/actions/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {EmojiTypes} from 'mattermost-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from 'mattermost-redux/actions/posts';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {EmojiTypes} from '@mm-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -79,7 +79,7 @@ export function getEmojisInPosts(posts) {
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_GET_EMOJIS_FOR_POSTS'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {FileTypes} from 'mattermost-redux/action_types';
|
||||
import {FileTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {buildFileUploadData, generateId} from 'app/utils/file';
|
||||
|
||||
@@ -3,36 +3,21 @@
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {getSessions} from 'mattermost-redux/actions/users';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {loadConfigAndLicense} from 'app/actions/views/root';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
return {
|
||||
type: ViewTypes.LOGIN_ID_CHANGED,
|
||||
loginId,
|
||||
};
|
||||
}
|
||||
|
||||
export function handlePasswordChanged(password) {
|
||||
return {
|
||||
type: ViewTypes.PASSWORD_CHANGED,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(loadConfigAndLicense());
|
||||
@@ -122,8 +107,6 @@ export function scheduleExpiredNotification(intl) {
|
||||
}
|
||||
|
||||
export default {
|
||||
handleLoginIdChanged,
|
||||
handlePasswordChanged,
|
||||
handleSuccessfulLogin,
|
||||
scheduleExpiredNotification,
|
||||
};
|
||||
|
||||
@@ -4,15 +4,9 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import * as GeneralActions from 'mattermost-redux/actions/general';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {
|
||||
handleLoginIdChanged,
|
||||
handlePasswordChanged,
|
||||
handleSuccessfulLogin,
|
||||
} from 'app/actions/views/login';
|
||||
import {handleSuccessfulLogin} from 'app/actions/views/login';
|
||||
|
||||
jest.mock('app/init/credentials', () => ({
|
||||
setAppCredentials: () => jest.fn(),
|
||||
@@ -51,31 +45,9 @@ describe('Actions.Views.Login', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('handleLoginIdChanged', () => {
|
||||
const loginId = 'email@example.com';
|
||||
|
||||
const action = {
|
||||
type: ViewTypes.LOGIN_ID_CHANGED,
|
||||
loginId,
|
||||
};
|
||||
store.dispatch(handleLoginIdChanged(loginId));
|
||||
expect(store.getActions()).toEqual([action]);
|
||||
});
|
||||
|
||||
test('handlePasswordChanged', () => {
|
||||
const password = 'password';
|
||||
const action = {
|
||||
type: ViewTypes.PASSWORD_CHANGED,
|
||||
password,
|
||||
};
|
||||
|
||||
store.dispatch(handlePasswordChanged(password));
|
||||
expect(store.getActions()).toEqual([action]);
|
||||
});
|
||||
|
||||
test('handleSuccessfulLogin gets config and license ', async () => {
|
||||
const getClientConfig = jest.spyOn(GeneralActions, 'getClientConfig');
|
||||
const getLicenseConfig = jest.spyOn(GeneralActions, 'getLicenseConfig');
|
||||
const getClientConfig = jest.spyOn(Client4, 'getClientConfigOld');
|
||||
const getLicenseConfig = jest.spyOn(Client4, 'getClientLicenseOld');
|
||||
|
||||
await store.dispatch(handleSuccessfulLogin());
|
||||
expect(getClientConfig).toHaveBeenCalled();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
|
||||
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||
import {getDirectChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {createDirectChannel, createGroupChannel} from '@mm-redux/actions/channels';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
|
||||
|
||||
export function makeDirectChannel(otherUserId, switchToChannel = true) {
|
||||
|
||||
@@ -3,23 +3,28 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {
|
||||
doPostAction,
|
||||
getNeededAtMentionedUsernames,
|
||||
receivedNewPost,
|
||||
receivedPost,
|
||||
receivedPosts,
|
||||
receivedPostsBefore,
|
||||
receivedPostsInChannel,
|
||||
receivedPostsSince,
|
||||
receivedPostsInThread,
|
||||
} from 'mattermost-redux/actions/posts';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {removeUserFromList} from 'mattermost-redux/utils/user_utils';
|
||||
} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Posts} from '@mm-redux/constants';
|
||||
import {getPost as selectPost, getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {isUnreadChannel, isArchivedChannel} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {generateId} from 'app/utils/file';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {generateId} from '@utils/file';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
|
||||
import {getEmojisInPosts} from './emoji';
|
||||
|
||||
@@ -87,8 +92,8 @@ export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
if (posts?.length) {
|
||||
actions.push(receivedPosts(data));
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +101,32 @@ export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
actions.push(receivedPostsInChannel(data, channelId, page === 0, data.prev_post_id === ''));
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POSTS'));
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPost(postId) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPost(postId);
|
||||
|
||||
if (data) {
|
||||
const actions = [
|
||||
receivedPost(data),
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch([data]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POST'));
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
@@ -118,11 +148,11 @@ export function getPostsSince(channelId, since) {
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POSTS_SINCE'));
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -145,11 +175,11 @@ export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POSTS_BEFORE'));
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -159,7 +189,7 @@ export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostThread(rootId) {
|
||||
export function getPostThread(rootId, skipDispatch = false) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = await Client4.getPostThread(rootId);
|
||||
@@ -172,11 +202,15 @@ export function getPostThread(rootId) {
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
if (skipDispatch) {
|
||||
return {data: actions};
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POSTS_THREAD'));
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -219,11 +253,11 @@ export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZ
|
||||
];
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.length) {
|
||||
actions.push(...additional);
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_GET_POSTS_AROUND'));
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -233,12 +267,40 @@ export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZ
|
||||
};
|
||||
}
|
||||
|
||||
function getPostsAdditionalDataBatch(posts = []) {
|
||||
export function handleNewPostBatch(WebSocketMessage) {
|
||||
return async (dispatch, getState) => {
|
||||
const actions = [];
|
||||
const state = getState();
|
||||
const post = JSON.parse(WebSocketMessage.data.post);
|
||||
const actions = [receivedNewPost(post)];
|
||||
|
||||
// If we don't have the thread for this post, fetch it from the server
|
||||
// and include the actions in the batch
|
||||
if (post.root_id) {
|
||||
const rootPost = selectPost(state, post.root_id);
|
||||
|
||||
if (!rootPost) {
|
||||
const thread = await dispatch(getPostThread(post.root_id, true));
|
||||
if (thread.actions?.length) {
|
||||
actions.push(...thread.actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch([post]));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsAdditionalDataBatch(posts = []) {
|
||||
return async (dispatch, getState) => {
|
||||
const data = [];
|
||||
|
||||
if (!posts.length) {
|
||||
return actions;
|
||||
return {data};
|
||||
}
|
||||
|
||||
// Custom Emojis used in the posts
|
||||
@@ -249,7 +311,7 @@ function getPostsAdditionalDataBatch(posts = []) {
|
||||
const state = getState();
|
||||
const promises = [];
|
||||
const promiseTrace = [];
|
||||
const extra = dispatch(profilesStatusesAndToLoadFromPosts(posts));
|
||||
const extra = userMetadataToLoadFromPosts(state, posts);
|
||||
|
||||
if (extra?.userIds.length) {
|
||||
promises.push(Client4.getProfilesByIds(extra.userIds));
|
||||
@@ -273,7 +335,7 @@ function getPostsAdditionalDataBatch(posts = []) {
|
||||
const type = promiseTrace[index];
|
||||
switch (type) {
|
||||
case 'statuses':
|
||||
actions.push({
|
||||
data.push({
|
||||
type: UserTypes.RECEIVED_STATUSES,
|
||||
data: p,
|
||||
});
|
||||
@@ -282,7 +344,7 @@ function getPostsAdditionalDataBatch(posts = []) {
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
removeUserFromList(currentUserId, p);
|
||||
actions.push({
|
||||
data.push({
|
||||
type: UserTypes.RECEIVED_PROFILES_LIST,
|
||||
data: p,
|
||||
});
|
||||
@@ -296,42 +358,123 @@ function getPostsAdditionalDataBatch(posts = []) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
return actions;
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
|
||||
function profilesStatusesAndToLoadFromPosts(posts = []) {
|
||||
return (dispatch, getState) => {
|
||||
function userMetadataToLoadFromPosts(state, posts = []) {
|
||||
const {currentUserId, profiles, statuses} = state.entities.users;
|
||||
|
||||
// Profiles of users mentioned in the posts
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(state, posts);
|
||||
|
||||
// Statuses and profiles of the users who made the posts
|
||||
const userIdsToLoad = new Set();
|
||||
const statusesToLoad = new Set();
|
||||
|
||||
posts.forEach((post) => {
|
||||
const userId = post.user_id;
|
||||
|
||||
if (!statuses[userId]) {
|
||||
statusesToLoad.add(userId);
|
||||
}
|
||||
|
||||
if (userId === currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profiles[userId]) {
|
||||
userIdsToLoad.add(userId);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
usernames: Array.from(usernamesToLoad),
|
||||
userIds: Array.from(userIdsToLoad),
|
||||
statuses: Array.from(statusesToLoad),
|
||||
};
|
||||
}
|
||||
|
||||
export function loadUnreadChannelPosts(channels, channelMembers) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentUserId, profiles, statuses} = state.entities.users;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
|
||||
// Profiles of users mentioned in the posts
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(state, posts);
|
||||
const promises = [];
|
||||
const promiseTrace = [];
|
||||
|
||||
// Statuses and profiles of the users who made the posts
|
||||
const userIdsToLoad = new Set();
|
||||
const statusesToLoad = new Set();
|
||||
const channelMembersByChannel = {};
|
||||
channelMembers.forEach((member) => {
|
||||
channelMembersByChannel[member.channel_id] = member;
|
||||
});
|
||||
|
||||
posts.forEach((post) => {
|
||||
const userId = post.user_id;
|
||||
|
||||
if (!statuses[userId]) {
|
||||
statusesToLoad.add(userId);
|
||||
}
|
||||
|
||||
if (userId === currentUserId) {
|
||||
channels.forEach((channel) => {
|
||||
if (channel.id === currentChannelId || isArchivedChannel(channel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!profiles[userId]) {
|
||||
userIdsToLoad.add(userId);
|
||||
const isUnread = isUnreadChannel(channelMembersByChannel, channel);
|
||||
if (!isUnread) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postIds = getPostIdsInChannel(state, channel.id);
|
||||
|
||||
let promise;
|
||||
const trace = {
|
||||
channelId: channel.id,
|
||||
since: false,
|
||||
};
|
||||
if (!postIds || !postIds.length) {
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
promise = Client4.getPosts(channel.id);
|
||||
} else {
|
||||
const since = getChannelSinceValue(state, channel.id, postIds);
|
||||
promise = Client4.getPostsSince(channel.id, since);
|
||||
trace.since = since;
|
||||
}
|
||||
|
||||
promises.push(promise);
|
||||
promiseTrace.push(trace);
|
||||
});
|
||||
|
||||
return {
|
||||
usernames: Array.from(usernamesToLoad),
|
||||
userIds: Array.from(userIdsToLoad),
|
||||
statuses: Array.from(statusesToLoad),
|
||||
};
|
||||
let posts = [];
|
||||
const actions = [];
|
||||
if (promises.length) {
|
||||
const results = await Promise.all(promises);
|
||||
results.forEach((data, index) => {
|
||||
const channelPosts = Object.values(data.posts);
|
||||
if (channelPosts.length) {
|
||||
posts = posts.concat(channelPosts);
|
||||
|
||||
const trace = promiseTrace[index];
|
||||
if (trace.since) {
|
||||
actions.push(receivedPostsSince(data, trace.channelId));
|
||||
} else {
|
||||
actions.push(receivedPostsInChannel(data, trace.channelId, true, data.prev_post_id === ''));
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
|
||||
channelId: trace.channelId,
|
||||
time: Date.now(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Fetched ${posts.length} posts from ${promises.length} unread channels`); //eslint-disable-line no-console
|
||||
if (posts.length) {
|
||||
// receivedPosts should be the first action dispatched as
|
||||
// receivedPostsSince and receivedPostsInChannel reducers are
|
||||
// dependent on it.
|
||||
actions.unshift(receivedPosts({posts}));
|
||||
const additional = await dispatch(getPostsAdditionalDataBatch(posts));
|
||||
if (additional.data.length) {
|
||||
actions.push(...additional.data);
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
186
app/actions/views/post.test.js
Normal file
186
app/actions/views/post.test.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {PostTypes, UserTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import * as ChannelUtils from '@mm-redux/utils/channel_utils';
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import initialState from '@store/initial_state';
|
||||
|
||||
import {loadUnreadChannelPosts} from '@actions/views/post';
|
||||
|
||||
describe('Actions.Views.Post', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
let store;
|
||||
const currentChannelId = 'current-channel-id';
|
||||
const storeObj = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
channels: {
|
||||
...initialState.entities.channels,
|
||||
currentChannelId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const channels = [
|
||||
{id: 'channel-1'},
|
||||
{id: 'channel-2'},
|
||||
{id: 'channel-3'},
|
||||
];
|
||||
const channelMembers = [];
|
||||
|
||||
beforeEach(() => {
|
||||
ChannelUtils.isUnreadChannel = jest.fn().mockReturnValue(true);
|
||||
ChannelUtils.isArchivedChannel = jest.fn().mockReturnValue(false);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts does not dispatch actions if no unread channels', async () => {
|
||||
ChannelUtils.isUnreadChannel = jest.fn().mockReturnValue(false);
|
||||
|
||||
store = mockStore(storeObj);
|
||||
await store.dispatch(loadUnreadChannelPosts(channels, channelMembers));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
expect(storeActions).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts does not dispatch actions for archived channels', async () => {
|
||||
ChannelUtils.isArchivedChannel = jest.fn().mockReturnValue(true);
|
||||
Client4.getPosts = jest.fn().mockResolvedValue({posts: ['post-1', 'post-2']});
|
||||
|
||||
store = mockStore(storeObj);
|
||||
await store.dispatch(loadUnreadChannelPosts(channels, channelMembers));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
expect(storeActions).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts does not dispatch actions for current channel', async () => {
|
||||
Client4.getPosts = jest.fn().mockResolvedValue({posts: ['post-1', 'post-2']});
|
||||
|
||||
store = mockStore(storeObj);
|
||||
await store.dispatch(loadUnreadChannelPosts([{id: currentChannelId}], channelMembers));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
expect(storeActions).toStrictEqual([]);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts dispatches actions for unread channels with no postIds in channel', async () => {
|
||||
Client4.getPosts = jest.fn().mockResolvedValue({posts: ['post-1', 'post-2']});
|
||||
|
||||
store = mockStore(storeObj);
|
||||
await store.dispatch(loadUnreadChannelPosts(channels, channelMembers));
|
||||
|
||||
const actionTypes = store.getActions()[0].payload.map((action) => action.type);
|
||||
|
||||
// Actions dispatched:
|
||||
// RECEIVED_POSTS once and first, with all channel posts combined.
|
||||
// RECEIVED_POSTS_IN_CHANNEL and RECEIVED_POSTS_FOR_CHANNEL_AT_TIME for each channel.
|
||||
expect(actionTypes.length).toBe((2 * channels.length) + 1);
|
||||
expect(actionTypes[0]).toEqual(PostTypes.RECEIVED_POSTS);
|
||||
|
||||
const receivedPostsInChannelActions = actionTypes.filter((type) => type === PostTypes.RECEIVED_POSTS_IN_CHANNEL);
|
||||
expect(receivedPostsInChannelActions.length).toBe(channels.length);
|
||||
|
||||
const receivedPostsForChannelAtTimeActions = actionTypes.filter((type) => type === ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME);
|
||||
expect(receivedPostsForChannelAtTimeActions.length).toBe(channels.length);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts dispatches actions for unread channels with postIds in channel', async () => {
|
||||
PostSelectors.getPostIdsInChannel = jest.fn().mockReturnValue(['post-id-in-channel']);
|
||||
Client4.getPostsSince = jest.fn().mockResolvedValue({posts: ['post-1', 'post-2']});
|
||||
|
||||
const lastGetPosts = {};
|
||||
channels.forEach((channel) => {
|
||||
lastGetPosts[channel.id] = Date.now();
|
||||
});
|
||||
const lastConnectAt = Date.now() + 1000;
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
views: {
|
||||
channel: {
|
||||
lastGetPosts,
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
lastConnectAt,
|
||||
},
|
||||
});
|
||||
await store.dispatch(loadUnreadChannelPosts(channels, channelMembers));
|
||||
|
||||
const actionTypes = store.getActions()[0].payload.map((action) => action.type);
|
||||
|
||||
// Actions dispatched:
|
||||
// RECEIVED_POSTS once and first, with all channel posts combined.
|
||||
// RECEIVED_POSTS_SINCE and RECEIVED_POSTS_FOR_CHANNEL_AT_TIME for each channel.
|
||||
expect(actionTypes.length).toBe((2 * channels.length) + 1);
|
||||
expect(actionTypes[0]).toEqual(PostTypes.RECEIVED_POSTS);
|
||||
|
||||
const receivedPostsInChannelActions = actionTypes.filter((type) => type === PostTypes.RECEIVED_POSTS_SINCE);
|
||||
expect(receivedPostsInChannelActions.length).toBe(channels.length);
|
||||
|
||||
const receivedPostsForChannelAtTimeActions = actionTypes.filter((type) => type === ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME);
|
||||
expect(receivedPostsForChannelAtTimeActions.length).toBe(channels.length);
|
||||
});
|
||||
|
||||
test('loadUnreadChannelPosts dispatches additional actions for unread channels', async () => {
|
||||
const posts = [{
|
||||
user_id: 'user-id',
|
||||
message: '@user post-1',
|
||||
}];
|
||||
PostSelectors.getPostIdsInChannel = jest.fn().mockReturnValue(['post-id-in-channel']);
|
||||
Client4.getPostsSince = jest.fn().mockResolvedValue({posts});
|
||||
Client4.getProfilesByIds = jest.fn().mockResolvedValue(['data']);
|
||||
Client4.getProfilesByUsernames = jest.fn().mockResolvedValue(['data']);
|
||||
Client4.getStatusesByIds = jest.fn().mockResolvedValue(['data']);
|
||||
|
||||
const lastGetPosts = {};
|
||||
channels.forEach((channel) => {
|
||||
lastGetPosts[channel.id] = Date.now();
|
||||
});
|
||||
const lastConnectAt = Date.now() + 1000;
|
||||
store = mockStore({
|
||||
...storeObj,
|
||||
views: {
|
||||
channel: {
|
||||
lastGetPosts,
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
lastConnectAt,
|
||||
},
|
||||
});
|
||||
await store.dispatch(loadUnreadChannelPosts(channels, channelMembers));
|
||||
|
||||
const actionTypes = store.getActions()[0].payload.map((action) => action.type);
|
||||
|
||||
// Actions dispatched:
|
||||
// RECEIVED_POSTS once and first, with all channel posts combined.
|
||||
// RECEIVED_POSTS_SINCE and RECEIVED_POSTS_FOR_CHANNEL_AT_TIME for each channel.
|
||||
// RECEIVED_PROFILES_LIST twice, once for getProfilesByIds and once for getProfilesByUsernames
|
||||
// RECEIVED_STATUSES for getStatusesByIds
|
||||
expect(actionTypes.length).toBe((2 * channels.length) + 4);
|
||||
expect(actionTypes[0]).toEqual(PostTypes.RECEIVED_POSTS);
|
||||
|
||||
const receivedPostsInChannelActions = actionTypes.filter((type) => type === PostTypes.RECEIVED_POSTS_SINCE);
|
||||
expect(receivedPostsInChannelActions.length).toBe(channels.length);
|
||||
|
||||
const receivedPostsForChannelAtTimeActions = actionTypes.filter((type) => type === ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME);
|
||||
expect(receivedPostsForChannelAtTimeActions.length).toBe(channels.length);
|
||||
|
||||
const receivedProfiles = actionTypes.filter((type) => type === UserTypes.RECEIVED_PROFILES_LIST);
|
||||
expect(receivedProfiles.length).toBe(2);
|
||||
|
||||
const receivedStatuses = actionTypes.filter((type) => type === UserTypes.RECEIVED_STATUSES);
|
||||
expect(receivedStatuses.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -3,17 +3,20 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels';
|
||||
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
|
||||
import {receivedNewPost} from 'mattermost-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers} from 'mattermost-redux/actions/teams';
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {receivedNewPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
import {recordTime} from 'app/utils/segment';
|
||||
import {NavigationTypes, ViewTypes} from '@constants';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import initialState from '@store/initial_state';
|
||||
import {getStateForReset} from '@store/utils';
|
||||
import {recordTime} from '@utils/segment';
|
||||
|
||||
import {markChannelViewedAndRead} from './channel';
|
||||
|
||||
@@ -29,24 +32,36 @@ export function startDataCleanup() {
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
const [configData, licenseData] = await Promise.all([
|
||||
getClientConfig()(dispatch, getState),
|
||||
getLicenseConfig()(dispatch, getState),
|
||||
]);
|
||||
|
||||
const config = configData.data || {};
|
||||
const license = licenseData.data || {};
|
||||
try {
|
||||
const [config, license] = await Promise.all([
|
||||
Client4.getClientConfigOld(),
|
||||
Client4.getClientLicenseOld(),
|
||||
]);
|
||||
|
||||
if (currentUserId) {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
const actions = [{
|
||||
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
|
||||
data: config,
|
||||
}, {
|
||||
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
|
||||
data: license,
|
||||
}];
|
||||
|
||||
if (currentUserId) {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {config, license};
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
|
||||
|
||||
return {config, license};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -116,7 +131,7 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
|
||||
}
|
||||
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
dispatch(batchActions(actions, 'BATCH_SELECT_TEAM_AND_CHANNEL'));
|
||||
}
|
||||
|
||||
EphemeralStore.setStartFromNotification(false);
|
||||
@@ -126,10 +141,19 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
|
||||
}
|
||||
|
||||
export function purgeOfflineStore() {
|
||||
return {type: General.OFFLINE_STORE_PURGE};
|
||||
return (dispatch, getState) => {
|
||||
const currentState = getState();
|
||||
|
||||
dispatch({
|
||||
type: General.OFFLINE_STORE_PURGE,
|
||||
state: getStateForReset(initialState, currentState),
|
||||
});
|
||||
|
||||
EventEmitter.emit(NavigationTypes.RESTART_APP);
|
||||
};
|
||||
}
|
||||
|
||||
// A non-optimistic version of the createPost action in mattermost-redux with the file handling
|
||||
// A non-optimistic version of the createPost action in app/mm-redux with the file handling
|
||||
// removed since it's not needed.
|
||||
export function createPostForNotificationReply(post) {
|
||||
return async (dispatch, getState) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -11,7 +11,7 @@ export function handleServerUrlChanged(serverUrl) {
|
||||
{type: GeneralTypes.CLIENT_CONFIG_RESET},
|
||||
{type: GeneralTypes.CLIENT_LICENSE_RESET},
|
||||
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl},
|
||||
]);
|
||||
], 'BATCH_SERVER_URL_CHANGED');
|
||||
}
|
||||
|
||||
export function setServerUrl(serverUrl) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('Actions.Views.SelectServer', () => {
|
||||
{type: GeneralTypes.CLIENT_CONFIG_RESET},
|
||||
{type: GeneralTypes.CLIENT_LICENSE_RESET},
|
||||
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl},
|
||||
]);
|
||||
], 'BATCH_SERVER_URL_CHANGED');
|
||||
|
||||
store.dispatch(handleServerUrlChanged(serverUrl));
|
||||
expect(store.getActions()).toEqual([actions]);
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
|
||||
import {getMyTeams} from 'mattermost-redux/actions/teams';
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {ChannelTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getMyTeams} from '@mm-redux/actions/teams';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {selectFirstAvailableTeam} from 'app/utils/teams';
|
||||
@@ -23,7 +23,7 @@ export function handleTeamChange(teamId) {
|
||||
dispatch(batchActions([
|
||||
{type: TeamTypes.SELECT_TEAM, data: teamId},
|
||||
{type: ChannelTypes.SELECT_CHANNEL, data: '', extra: {}},
|
||||
]));
|
||||
], 'BATCH_SWITCH_TEAM'));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleCommentDraftChanged(rootId, draft) {
|
||||
return {
|
||||
type: ViewTypes.COMMENT_DRAFT_CHANGED,
|
||||
rootId,
|
||||
draft,
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.views.thread.drafts[rootId]?.draft !== draft) {
|
||||
dispatch({
|
||||
type: ViewTypes.COMMENT_DRAFT_CHANGED,
|
||||
rootId,
|
||||
draft,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ describe('Actions.Views.Thread', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({});
|
||||
store = mockStore({
|
||||
views: {
|
||||
thread: {
|
||||
drafts: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('handleCommentDraftChanged', () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
|
||||
import {userTyping as wsUserTyping} from '@actions/websocket';
|
||||
|
||||
export function userTyping(channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const {websocket} = getState();
|
||||
const state = getState();
|
||||
const {websocket} = state;
|
||||
if (websocket.connected) {
|
||||
wsUserTyping(channelId, rootId)(dispatch, getState);
|
||||
wsUserTyping(state, channelId, rootId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {GeneralTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
|
||||
import * as HelperActions from 'mattermost-redux/actions/helpers';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {NavigationTypes} from 'app/constants';
|
||||
import {GeneralTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import * as HelperActions from '@mm-redux/actions/helpers';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezoneAsync} from '@utils/timezone';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
@@ -26,7 +29,6 @@ export function completeLogin(user, deviceToken) {
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
|
||||
setCSRFFromCookie(url);
|
||||
setAppCredentials(deviceToken, user.id, token, url);
|
||||
|
||||
// Set timezone
|
||||
@@ -46,14 +48,42 @@ export function completeLogin(user, deviceToken) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMe(user, deviceToken) {
|
||||
export function getMe() {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const data = {};
|
||||
data.me = await Client4.getMe();
|
||||
|
||||
const actions = [{
|
||||
type: UserTypes.RECEIVED_ME,
|
||||
data: data.me,
|
||||
}];
|
||||
|
||||
const roles = data.me.roles.split(' ');
|
||||
data.roles = await Client4.getRolesByNames(roles);
|
||||
if (data.roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_GET_ME'));
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadMe(user, deviceToken, skipDispatch = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const data = {user};
|
||||
const deviceId = state.entities?.general?.deviceToken;
|
||||
|
||||
try {
|
||||
if (deviceId && !deviceToken) {
|
||||
if (deviceId && !deviceToken && !skipDispatch) {
|
||||
await Client4.attachDevice(deviceId);
|
||||
}
|
||||
|
||||
@@ -75,6 +105,7 @@ export function loadMe(user, deviceToken) {
|
||||
const teamUnreadRequest = Client4.getMyTeamUnreads();
|
||||
const preferencesRequest = Client4.getMyPreferences();
|
||||
const configRequest = Client4.getClientConfigOld();
|
||||
const actions = [];
|
||||
|
||||
const [teams, teamMembers, teamUnreads, preferences, config] = await Promise.all([
|
||||
teamsRequest,
|
||||
@@ -91,22 +122,33 @@ export function loadMe(user, deviceToken) {
|
||||
data.config = config;
|
||||
data.url = Client4.getUrl();
|
||||
|
||||
dispatch({
|
||||
actions.push({
|
||||
type: UserTypes.LOGIN,
|
||||
data,
|
||||
});
|
||||
|
||||
const roles = new Set();
|
||||
const rolesToLoad = new Set();
|
||||
for (const role of data.user.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
|
||||
for (const teamMember of teamMembers) {
|
||||
for (const role of teamMember.roles.split(' ')) {
|
||||
roles.add(role);
|
||||
rolesToLoad.add(role);
|
||||
}
|
||||
}
|
||||
if (roles.size > 0) {
|
||||
dispatch(loadRolesIfNeeded(roles));
|
||||
if (rolesToLoad.size > 0) {
|
||||
data.roles = await Client4.getRolesByNames(Array.from(rolesToLoad));
|
||||
if (data.roles.length) {
|
||||
actions.push({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data: data.roles,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipDispatch) {
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_ME'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('login error', error.stack); // eslint-disable-line no-console
|
||||
@@ -125,6 +167,7 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
|
||||
|
||||
try {
|
||||
user = await Client4.login(loginId, password, mfaToken, deviceToken, ldapOnly);
|
||||
await setCSRFFromCookie(Client4.getUrl());
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
@@ -142,6 +185,7 @@ export function login(loginId, password, mfaToken, ldapOnly = false) {
|
||||
export function ssoLogin(token) {
|
||||
return async (dispatch) => {
|
||||
Client4.setToken(token);
|
||||
await setCSRFFromCookie(Client4.getUrl());
|
||||
const result = await dispatch(loadMe());
|
||||
|
||||
if (!result.error) {
|
||||
@@ -153,7 +197,7 @@ export function ssoLogin(token) {
|
||||
}
|
||||
|
||||
export function logout(skipServerLogout = false) {
|
||||
return async (dispatch) => {
|
||||
return async () => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
Client4.logout();
|
||||
@@ -162,16 +206,14 @@ export function logout(skipServerLogout = false) {
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({type: UserTypes.LOGOUT_SUCCESS});
|
||||
EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function forceLogoutIfNecessary(error) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
|
||||
if (currentUserId && error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
|
||||
return async (dispatch) => {
|
||||
if (error.status_code === HTTP_UNAUTHORIZED && error.url && !error.url.includes('/login')) {
|
||||
dispatch(logout(true));
|
||||
return true;
|
||||
}
|
||||
@@ -182,15 +224,19 @@ export function forceLogoutIfNecessary(error) {
|
||||
|
||||
export function setCurrentUserStatusOffline() {
|
||||
return (dispatch, getState) => {
|
||||
const currentUserId = getCurrentUserId(getState());
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const status = getStatusForUserId(state, currentUserId);
|
||||
|
||||
return dispatch({
|
||||
type: UserTypes.RECEIVED_STATUS,
|
||||
data: {
|
||||
user_id: currentUserId,
|
||||
status: General.OFFLINE,
|
||||
},
|
||||
});
|
||||
if (status !== General.OFFLINE) {
|
||||
dispatch({
|
||||
type: UserTypes.RECEIVED_STATUS,
|
||||
data: {
|
||||
user_id: currentUserId,
|
||||
status: General.OFFLINE,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {UserTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import {setCurrentUserStatusOffline} from 'app/actions/views/user';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
jest.mock('mattermost-redux/actions/users', () => ({
|
||||
jest.mock('@mm-redux/actions/users', () => ({
|
||||
getStatus: (...args) => ({type: 'MOCK_GET_STATUS', args}),
|
||||
getStatusesByIds: (...args) => ({type: 'MOCK_GET_STATUS_BY_IDS', args}),
|
||||
startPeriodicStatusUpdates: () => ({type: 'MOCK_PERIODIC_STATUS_UPDATES'}),
|
||||
|
||||
1092
app/actions/websocket.test.js
Normal file
1092
app/actions/websocket.test.js
Normal file
File diff suppressed because it is too large
Load Diff
1181
app/actions/websocket.ts
Normal file
1181
app/actions/websocket.ts
Normal file
File diff suppressed because it is too large
Load Diff
247
app/client/websocket.ts
Normal file
247
app/client/websocket.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
|
||||
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
class WebSocketClient {
|
||||
conn?: WebSocket;
|
||||
connectionUrl: string;
|
||||
token: string|null;
|
||||
sequence: number;
|
||||
connectFailCount: number;
|
||||
eventCallback?: Function;
|
||||
firstConnectCallback?: Function;
|
||||
reconnectCallback?: Function;
|
||||
errorCallback?: Function;
|
||||
closeCallback?: Function;
|
||||
connectingCallback?: Function;
|
||||
stop: boolean;
|
||||
connectionTimeout: any;
|
||||
|
||||
constructor() {
|
||||
this.connectionUrl = '';
|
||||
this.token = null;
|
||||
this.sequence = 1;
|
||||
this.connectFailCount = 0;
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
initialize(token: string|null, opts = {}) {
|
||||
const defaults = {
|
||||
forceConnection: true,
|
||||
connectionUrl: this.connectionUrl,
|
||||
};
|
||||
|
||||
const {connectionUrl, forceConnection, ...additionalOptions} = Object.assign({}, defaults, opts);
|
||||
|
||||
if (forceConnection) {
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.conn) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionUrl == null) {
|
||||
console.log('websocket must have connection url'); //eslint-disable-line no-console
|
||||
reject(new Error('websocket must have connection url'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
}
|
||||
|
||||
const regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/;
|
||||
const captured = (regex).exec(connectionUrl);
|
||||
|
||||
let origin;
|
||||
if (captured) {
|
||||
origin = captured[0];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// this is done cause for android having the port 80 or 443 will fail the connection
|
||||
// the websocket will append them
|
||||
const split = origin.split(':');
|
||||
const port = split[2];
|
||||
if (port === '80' || port === '443') {
|
||||
origin = `${split[0]}:${split[1]}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway
|
||||
const errorMessage = 'websocket failed to parse origin from ' + connectionUrl;
|
||||
console.warn(errorMessage); // eslint-disable-line no-console
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
this.conn = new WebSocket(connectionUrl, [], {headers: {origin}, ...(additionalOptions || {})});
|
||||
this.connectionUrl = connectionUrl;
|
||||
this.token = token;
|
||||
|
||||
this.conn!.onopen = () => {
|
||||
if (token) {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
this.sendMessage('authentication_challenge', {token});
|
||||
}
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||
if (this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
this.firstConnectCallback();
|
||||
}
|
||||
|
||||
this.connectFailCount = 0;
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.conn!.onclose = () => {
|
||||
this.conn = undefined;
|
||||
this.sequence = 1;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
this.connectFailCount++;
|
||||
|
||||
if (this.closeCallback) {
|
||||
this.closeCallback(this.connectFailCount);
|
||||
}
|
||||
|
||||
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
|
||||
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
this.initialize(token, opts);
|
||||
},
|
||||
retryTime,
|
||||
);
|
||||
};
|
||||
|
||||
this.conn!.onerror = (evt: any) => {
|
||||
if (this.connectFailCount <= 1) {
|
||||
console.log('websocket error'); //eslint-disable-line no-console
|
||||
console.log(evt); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.errorCallback) {
|
||||
this.errorCallback(evt);
|
||||
}
|
||||
};
|
||||
|
||||
this.conn!.onmessage = (evt: any) => {
|
||||
const msg = JSON.parse(evt.data);
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg); //eslint-disable-line no-console
|
||||
}
|
||||
} else if (this.eventCallback) {
|
||||
this.eventCallback(msg);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setConnectingCallback(callback: Function) {
|
||||
this.connectingCallback = callback;
|
||||
}
|
||||
|
||||
setEventCallback(callback: Function) {
|
||||
this.eventCallback = callback;
|
||||
}
|
||||
|
||||
setFirstConnectCallback(callback: Function) {
|
||||
this.firstConnectCallback = callback;
|
||||
}
|
||||
|
||||
setReconnectCallback(callback: Function) {
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
|
||||
setErrorCallback(callback: Function) {
|
||||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
setCloseCallback(callback: Function) {
|
||||
this.closeCallback = callback;
|
||||
}
|
||||
|
||||
close(stop = false) {
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.sequence = 1;
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.onclose = () => {}; //eslint-disable-line no-empty-function
|
||||
this.conn.close();
|
||||
this.conn = undefined;
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(action: string, data: any) {
|
||||
const msg = {
|
||||
action,
|
||||
seq: this.sequence++,
|
||||
data,
|
||||
};
|
||||
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.send(JSON.stringify(msg));
|
||||
} else if (!this.conn || this.conn.readyState === WebSocket.CLOSED) {
|
||||
this.conn = undefined;
|
||||
this.initialize(this.token);
|
||||
}
|
||||
}
|
||||
|
||||
userTyping(channelId: string, parentId: string) {
|
||||
this.sendMessage('user_typing', {
|
||||
channel_id: channelId,
|
||||
parent_id: parentId,
|
||||
});
|
||||
}
|
||||
|
||||
getStatuses() {
|
||||
this.sendMessage('get_statuses', null);
|
||||
}
|
||||
|
||||
getStatusesByIds(userIds: string[]) {
|
||||
this.sendMessage('get_statuses_by_ids', {
|
||||
user_ids: userIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketClient();
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import AnnouncementBanner from './announcement_banner.js';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
|
||||
import {Clipboard, Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getUsersByUsername, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getUsersByUsername, getCurrentUserMentionKeys} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import DocumentPicker from 'react-native-document-picker';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
import {lookupMimeType} from '@mm-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import emmProvider from 'app/init/emm_provider';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {shallow} from 'enzyme';
|
||||
import Permissions from 'react-native-permissions';
|
||||
import {Alert, StatusBar} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {SectionList} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
import {autocompleteUsers} from 'mattermost-redux/actions/users';
|
||||
import {getCurrentChannelId, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {autocompleteUsers} from '@mm-redux/actions/users';
|
||||
import {getCurrentChannelId, getDefaultChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
filterMembersInCurrentTeam,
|
||||
getMatchTermForAtMention,
|
||||
} from 'app/selectors/autocomplete';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {haveIChannelPermission} from '@mm-redux/selectors/entities/roles';
|
||||
import {Permissions} from '@mm-redux/constants';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
@@ -34,6 +34,7 @@ function mapStateToProps(state, ownProps) {
|
||||
{
|
||||
channel: currentChannelId,
|
||||
permission: Permissions.USE_CHANNEL_MENTIONS,
|
||||
default: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AutocompleteDivider from './autocomplete_divider';
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, SectionList} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {debounce} from '@mm-redux/actions/helpers';
|
||||
|
||||
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {searchChannels, autocompleteChannelsForSearch} from 'mattermost-redux/actions/channels';
|
||||
import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import {
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
filterDirectAndGroupMessages,
|
||||
getMatchTermForChannelMention,
|
||||
} from 'app/selectors/autocomplete';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import ChannelMention from './channel_mention';
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Text,
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import {BotTag, GuestTag} from 'app/components/tag';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getUser} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getChannelNameForSearchAutocomplete} from 'app/selectors/channel';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
||||
import {CalendarList, LocaleConfig} from 'react-native-calendars';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {memoizeResult} from 'mattermost-redux/utils/helpers';
|
||||
import {memoizeResult} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {DATE_MENTION_SEARCH_REGEX, ALL_SEARCH_FLAGS_REGEX} from 'app/constants/autocomplete';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {makeGetMatchTermForDateMention} from 'app/selectors/autocomplete';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
|
||||
@@ -5,11 +5,11 @@ import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {autocompleteCustomEmojis} from 'mattermost-redux/actions/emojis';
|
||||
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
|
||||
import {autocompleteCustomEmojis} from '@mm-redux/actions/emojis';
|
||||
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {EmojiIndicesByAlias} from 'app/utils/emojis';
|
||||
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations';
|
||||
import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getAutocompleteCommands} from '@mm-redux/actions/integrations';
|
||||
import {getAutocompleteCommandsList} from '@mm-redux/selectors/entities/integrations';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class SlashSuggestionItem extends PureComponent {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
row: {
|
||||
height: 55,
|
||||
paddingVertical: 8,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
|
||||
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
||||
import {intlShape} from 'react-intl';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {setAutocompleteSelector} from 'app/actions/views/post';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import Icon from 'app/components/vector_icon';
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import {goToScreen} from 'app/actions/navigation';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import {getChannelMembersForDm} from 'app/selectors/channel';
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {joinChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getChannelsNameMapInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {joinChannel} from '@mm-redux/actions/channels';
|
||||
import {getChannelsNameMapInCurrentTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {handleSelectChannel} from 'app/actions/views/channel';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from 'react-native';
|
||||
import {ImageContent} from 'rn-placeholder';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import ChannelLoader from './channel_loader';
|
||||
|
||||
jest.mock('rn-placeholder', () => ({
|
||||
ImageContent: () => {},
|
||||
ImageContent: () => null,
|
||||
}));
|
||||
|
||||
describe('ChannelLoader', () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
|
||||
import getClientUpgrade from 'app/selectors/client_upgrade';
|
||||
|
||||
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {Posts} from '@mm-redux/constants';
|
||||
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getMissingProfilesByIds, getMissingProfilesByUsernames} from 'mattermost-redux/actions/users';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getMissingProfilesByIds, getMissingProfilesByUsernames} from '@mm-redux/actions/users';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getBool} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, makeGetProfilesByIdsAndUsernames} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import CombinedSystemMessage from './combined_system_message';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {Posts} from '@mm-redux/constants';
|
||||
|
||||
import FormattedMarkdownText from 'app/components/formatted_markdown_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {makeGenerateCombinedPost} from 'mattermost-redux/utils/post_list';
|
||||
import {makeGenerateCombinedPost} from '@mm-redux/utils/post_list';
|
||||
|
||||
import Post from 'app/components/post';
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {makeGetChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import ChannelListRow from './channel_list_row';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import CustomList, {FLATLIST, SECTIONLIST} from './index';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import OptionListRow from './option_list_row';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {isLandscape} from 'app/selectors/device';
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import CustomListRow from 'app/components/custom_list/custom_list_row';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import UserListRow from './user_list_row';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'react-native';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import ErrorText from 'app/components/error_text';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import Autocomplete from 'app/components/autocomplete';
|
||||
import EditChannelInfo from './edit_channel_info';
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {isMinimumServerVersion} from 'mattermost-redux/utils/helpers';
|
||||
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {BuiltInEmojis, EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
import {filterEmojiSearchInput} from './emoji_picker_base';
|
||||
|
||||
@@ -5,10 +5,10 @@ import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emojis';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCustomEmojisByName} from '@mm-redux/selectors/entities/emojis';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCustomEmojis, searchCustomEmojis} from '@mm-redux/actions/emojis';
|
||||
|
||||
import {incrementEmojiPickerPage} from 'app/actions/views/emoji';
|
||||
import {getDimensions, isLandscape} from 'app/selectors/device';
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getDisplayableErrors} from 'mattermost-redux/selectors/errors';
|
||||
import {dismissError, clearErrors} from 'mattermost-redux/actions/errors';
|
||||
import {getDisplayableErrors} from '@mm-redux/selectors/errors';
|
||||
import {dismissError, clearErrors} from '@mm-redux/actions/errors';
|
||||
|
||||
import ErrorList from './error_list';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import ErrorText from './error_text.js';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import ErrorText from './error_text.js';
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`FileAttachmentList should match snapshot with a single image file 1`] =
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -109,7 +109,7 @@ exports[`FileAttachmentList should match snapshot with combination of image and
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -332,7 +332,7 @@ exports[`FileAttachmentList should match snapshot with four image files 1`] = `
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -637,7 +637,7 @@ exports[`FileAttachmentList should match snapshot with more than four image file
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1014,7 +1014,7 @@ exports[`FileAttachmentList should match snapshot with three image files 1`] = `
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1248,7 +1248,7 @@ exports[`FileAttachmentList should match snapshot with two image files 1`] = `
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"width": undefined,
|
||||
"width": 680,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils.js';
|
||||
import * as Utils from '@mm-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {isDocument, isGif} from 'app/utils/file';
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import FileAttachment from './file_attachment.js';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
jest.mock('react-native-doc-viewer', () => ({
|
||||
openDoc: jest.fn(),
|
||||
|
||||
@@ -19,7 +19,7 @@ import {CircularProgress} from 'react-native-circular-progress';
|
||||
import {intlShape} from 'react-intl';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import {getFileUrl} from 'mattermost-redux/utils/file_utils.js';
|
||||
import {getFileUrl} from '@mm-redux/utils/file_utils';
|
||||
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
|
||||
import * as Utils from 'mattermost-redux/utils/file_utils';
|
||||
import * as Utils from '@mm-redux/utils/file_utils';
|
||||
|
||||
import audioIcon from 'assets/images/icons/audio.png';
|
||||
import codeIcon from 'assets/images/icons/code.png';
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
View,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import ProgressiveImage from 'app/components/progressive_image';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {isGif} from 'app/utils/file';
|
||||
import ProgressiveImage from '@components/progressive_image';
|
||||
import {isGif} from '@utils/file';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import thumb from 'assets/images/thumb.png';
|
||||
import brokenImageIcon from 'assets/images/icons/brokenimage.png';
|
||||
|
||||
const SMALL_IMAGE_MAX_HEIGHT = 48;
|
||||
const SMALL_IMAGE_MAX_WIDTH = 48;
|
||||
@@ -52,35 +50,9 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
resizeMethod: 'resize',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const {file} = props;
|
||||
if (file && file.id && !file.localPath) {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'X-CSRF-Token': Client4.csrf,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
const preloadImages = [
|
||||
{uri: Client4.getFileThumbnailUrl(file.id), headers},
|
||||
{uri: Client4.getFileUrl(file.id), headers},
|
||||
];
|
||||
|
||||
if (isGif(file)) {
|
||||
preloadImages.push({uri: Client4.getFilePreviewUrl(file.id), headers});
|
||||
}
|
||||
|
||||
FastImage.preload(preloadImages);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
opacity: new Animated.Value(0),
|
||||
requesting: true,
|
||||
retry: 0,
|
||||
};
|
||||
}
|
||||
state = {
|
||||
failed: false,
|
||||
};
|
||||
|
||||
boxPlaceholder = () => {
|
||||
if (this.props.isSingleImage) {
|
||||
@@ -97,9 +69,17 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleError = () => {
|
||||
this.setState({failed: true});
|
||||
}
|
||||
|
||||
imageProps = (file) => {
|
||||
const imageProps = {};
|
||||
if (file.localPath) {
|
||||
const {failed} = this.state;
|
||||
|
||||
if (failed) {
|
||||
imageProps.defaultSource = brokenImageIcon;
|
||||
} else if (file.localPath) {
|
||||
imageProps.defaultSource = {uri: file.localPath};
|
||||
} else if (file.id) {
|
||||
imageProps.thumbnailUri = Client4.getFileThumbnailUrl(file.id);
|
||||
@@ -134,9 +114,9 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
<View style={style.smallImageOverlay}>
|
||||
<ProgressiveImage
|
||||
style={{height: file.height, width: file.width}}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
onError={this.handleError}
|
||||
resizeMode={'contain'}
|
||||
resizeMethod={resizeMethod}
|
||||
{...this.imageProps(file)}
|
||||
@@ -168,9 +148,9 @@ export default class FileAttachmentImage extends PureComponent {
|
||||
{this.boxPlaceholder()}
|
||||
<ProgressiveImage
|
||||
style={[this.props.isSingleImage ? null : style.imagePreview, imageDimensions]}
|
||||
defaultSource={thumb}
|
||||
tintDefaultSource={!file.localPath}
|
||||
tintDefaultSource={!file.localPath && !this.state.failed}
|
||||
filename={file.name}
|
||||
onError={this.handleError}
|
||||
resizeMode={resizeMode}
|
||||
resizeMethod={resizeMethod}
|
||||
{...imageProps}
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {TABLET_WIDTH} from 'app/components/sidebars/drawer_layout';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import {isDocument, isGif, isVideo} from 'app/utils/file';
|
||||
import {previewImageAtIndex} from 'app/utils/images';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import ImageViewPort from '@components/image_viewport';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isDocument, isGif, isVideo} from '@utils/file';
|
||||
import {getViewPortWidth, previewImageAtIndex} from '@utils/images';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
import FileAttachment from './file_attachment';
|
||||
|
||||
const MAX_VISIBLE_ROW_IMAGES = 4;
|
||||
const VIEWPORT_IMAGE_OFFSET = 70;
|
||||
const VIEWPORT_IMAGE_REPLY_OFFSET = 11;
|
||||
|
||||
export default class FileAttachmentList extends PureComponent {
|
||||
export default class FileAttachmentList extends ImageViewPort {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
loadFilesForPostIfNecessary: PropTypes.func.isRequired,
|
||||
@@ -41,8 +34,6 @@ export default class FileAttachmentList extends PureComponent {
|
||||
files: [],
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -55,37 +46,24 @@ export default class FileAttachmentList extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
const {files} = this.props;
|
||||
|
||||
this.mounted = true;
|
||||
this.handlePermanentSidebar();
|
||||
this.handleDimensions();
|
||||
EventEmitter.on(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.addEventListener('change', this.handleDimensions);
|
||||
|
||||
if (files.length === 0) {
|
||||
this.loadFilesForPost();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.files !== this.props.files) {
|
||||
if (prevProps.files.length !== this.props.files.length) {
|
||||
this.filesForGallery = this.getFilesForGallery(this.props);
|
||||
this.buildGalleryFiles().then((results) => {
|
||||
this.galleryFiles = results;
|
||||
});
|
||||
}
|
||||
if (this.props.files !== prevProps.files && this.props.files.length === 0) {
|
||||
this.loadFilesForPost();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
EventEmitter.off(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS, this.handlePermanentSidebar);
|
||||
Dimensions.removeEventListener('change', this.handleDimensions);
|
||||
}
|
||||
|
||||
attachmentIndex = (fileId) => {
|
||||
return this.filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
@@ -149,45 +127,10 @@ export default class FileAttachmentList extends PureComponent {
|
||||
return results;
|
||||
};
|
||||
|
||||
getPortraitPostWidth = () => {
|
||||
const {isReplyPost} = this.props;
|
||||
const {width, height} = Dimensions.get('window');
|
||||
const permanentSidebar = DeviceTypes.IS_TABLET && !this.state?.isSplitView && this.state?.permanentSidebar;
|
||||
let portraitPostWidth = Math.min(width, height) - VIEWPORT_IMAGE_OFFSET;
|
||||
|
||||
if (permanentSidebar) {
|
||||
portraitPostWidth -= TABLET_WIDTH;
|
||||
}
|
||||
|
||||
if (isReplyPost) {
|
||||
portraitPostWidth -= VIEWPORT_IMAGE_REPLY_OFFSET;
|
||||
}
|
||||
|
||||
return portraitPostWidth;
|
||||
};
|
||||
|
||||
handleCaptureRef = (ref, idx) => {
|
||||
this.items[idx] = ref;
|
||||
};
|
||||
|
||||
handleDimensions = () => {
|
||||
if (this.mounted) {
|
||||
if (DeviceTypes.IS_TABLET) {
|
||||
mattermostManaged.isRunningInSplitView().then((result) => {
|
||||
const isSplitView = Boolean(result.isSplitView);
|
||||
this.setState({isSplitView});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handlePermanentSidebar = async () => {
|
||||
if (DeviceTypes.IS_TABLET && this.mounted) {
|
||||
const enabled = await AsyncStorage.getItem(DeviceTypes.PERMANENT_SIDEBAR_SETTINGS);
|
||||
this.setState({permanentSidebar: enabled === 'true'});
|
||||
}
|
||||
};
|
||||
|
||||
handlePreviewPress = preventDoubleTap((idx) => {
|
||||
previewImageAtIndex(this.items, idx, this.galleryFiles);
|
||||
});
|
||||
@@ -201,7 +144,7 @@ export default class FileAttachmentList extends PureComponent {
|
||||
}
|
||||
|
||||
renderItems = (items, moreImagesCount, includeGutter = false) => {
|
||||
const {canDownloadFiles, onLongPress, theme} = this.props;
|
||||
const {canDownloadFiles, isReplyPost, onLongPress, theme} = this.props;
|
||||
const isSingleImage = this.isSingleImage(items);
|
||||
let nonVisibleImagesCount;
|
||||
let container = styles.container;
|
||||
@@ -238,7 +181,7 @@ export default class FileAttachmentList extends PureComponent {
|
||||
theme={theme}
|
||||
isSingleImage={isSingleImage}
|
||||
nonVisibleImagesCount={nonVisibleImagesCount}
|
||||
wrapperWidth={this.getPortraitPostWidth()}
|
||||
wrapperWidth={getViewPortWidth(isReplyPost, this.hasPermanentSidebar())}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -250,8 +193,9 @@ export default class FileAttachmentList extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {isReplyPost} = this.props;
|
||||
const visibleImages = images.slice(0, MAX_VISIBLE_ROW_IMAGES);
|
||||
const {portraitPostWidth} = this.state;
|
||||
const portraitPostWidth = getViewPortWidth(isReplyPost, this.hasPermanentSidebar());
|
||||
|
||||
let nonVisibleImagesCount;
|
||||
if (images.length > MAX_VISIBLE_ROW_IMAGES) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import FileAttachment from './file_attachment_list.js';
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
jest.mock('react-native-doc-viewer', () => ({
|
||||
openDoc: jest.fn(),
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {canDownloadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
|
||||
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {canDownloadFilesOnMobile} from '@mm-redux/selectors/entities/general';
|
||||
import {makeGetFilesForPost} from '@mm-redux/selectors/entities/files';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
|
||||
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text, View} from 'react-native';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import FileAttachmentImage from '@components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from '@components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from '@components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from '@components/file_upload_preview/file_upload_remove';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from '@utils/file';
|
||||
import {emptyFunction} from '@utils/general';
|
||||
import ImageCacheManager from '@utils/image_cache_manager';
|
||||
|
||||
import FileAttachmentImage from 'app/components/file_attachment_list/file_attachment_image';
|
||||
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
|
||||
import FileUploadRetry from 'app/components/file_upload_preview/file_upload_retry';
|
||||
import FileUploadRemove from 'app/components/file_upload_preview/file_upload_remove';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class FileUploadItem extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -132,10 +132,8 @@ export default class FileUploadItem extends PureComponent {
|
||||
const fileData = buildFileUploadData(file);
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${Client4.getToken()}`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
...Client4.getOptions({method: 'post'}).headers,
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'X-CSRF-Token': Client4.csrf,
|
||||
};
|
||||
|
||||
const fileInfo = {
|
||||
@@ -155,7 +153,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
|
||||
const certificate = await mattermostBucket.getPreference('cert');
|
||||
const options = {
|
||||
timeout: 10000,
|
||||
timeout: 60000,
|
||||
certificate,
|
||||
};
|
||||
this.uploadPromise = RNFetchBlob.config(options).fetch('POST', Client4.getFilesRoute(), headers, data);
|
||||
@@ -164,7 +162,6 @@ export default class FileUploadItem extends PureComponent {
|
||||
};
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const styles = getStyleSheet(this.props.theme);
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
return (
|
||||
@@ -184,7 +181,6 @@ export default class FileUploadItem extends PureComponent {
|
||||
theme,
|
||||
} = this.props;
|
||||
const {progress} = this.state;
|
||||
const styles = getStyleSheet(theme);
|
||||
let filePreviewComponent;
|
||||
|
||||
if (this.isImageType()) {
|
||||
@@ -193,6 +189,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
resizeMode='center'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -249,7 +246,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
const styles = StyleSheet.create({
|
||||
preview: {
|
||||
paddingTop: 5,
|
||||
marginLeft: 12,
|
||||
@@ -274,10 +271,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
filePreview: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import FileUploadItem from './file_upload_item';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleRemoveFile, retryFileUpload, uploadComplete, uploadFailed} from 'app/actions/views/file_upload';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {checkForFileUploadingInChannel} from 'app/selectors/file';
|
||||
|
||||
@@ -92,7 +92,7 @@ class FormattedMarkdownText extends React.PureComponent {
|
||||
}
|
||||
|
||||
renderLink = ({children, href}) => {
|
||||
var url = href[0] === TARGET_BLANK_URL_PREFIX ? href.substring(1, href.length) : href;
|
||||
const url = href[0] === TARGET_BLANK_URL_PREFIX ? href.substring(1, href.length) : href;
|
||||
return <MarkdownLink href={url}>{children}</MarkdownLink>;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user