forked from Ivasoft/mattermost-mobile
Compare commits
10 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce283324ae | ||
|
|
3e54975684 | ||
|
|
b14dafa57e | ||
|
|
121656038c | ||
|
|
064b1883ce | ||
|
|
1282ff1e8e | ||
|
|
6adbc03faa | ||
|
|
836dc521b4 | ||
|
|
8a3eb36911 | ||
|
|
a7dfc99cf6 |
@@ -1,7 +1,6 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
owasp: entur/owasp@0.0.10
|
||||
node: circleci/node@4.4.0
|
||||
|
||||
executors:
|
||||
android:
|
||||
@@ -24,7 +23,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "12.1.0"
|
||||
xcode: "12.0.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
|
||||
@@ -92,8 +91,6 @@ commands:
|
||||
npm-dependencies:
|
||||
description: "Get JavaScript dependencies"
|
||||
steps:
|
||||
- node/install-npm:
|
||||
version: '6.14.11'
|
||||
- restore_cache:
|
||||
name: Restore npm cache
|
||||
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}
|
||||
@@ -553,14 +550,14 @@ workflows:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^(build|android)-pr-.*/
|
||||
only: /^build-pr-.*/
|
||||
- build-ios-pr:
|
||||
context: mattermost-mobile-ios-pr
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
only: /^(build|ios)-pr-.*/
|
||||
only: /^build-pr-.*/
|
||||
|
||||
- build-android-unsigned:
|
||||
context: mattermost-mobile-unsigned
|
||||
@@ -590,7 +587,6 @@ workflows:
|
||||
- /^build-\d+$/
|
||||
- /^build-ios-\d+$/
|
||||
- /^build-ios-beta-\d+$/
|
||||
- /^build-ios-sim-\d+$/
|
||||
|
||||
- github-release:
|
||||
context: mattermost-mobile-unsigned
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"mattermost"
|
||||
"mattermost",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
|
||||
13
.flowconfig
13
.flowconfig
@@ -8,6 +8,10 @@
|
||||
; Ignore polyfills
|
||||
node_modules/react-native/Libraries/polyfills/.*
|
||||
|
||||
; These should not be required directly
|
||||
; require from fbjs/lib instead: require('fbjs/lib/warning')
|
||||
node_modules/warning/.*
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/LoadingView.js
|
||||
|
||||
@@ -26,8 +30,6 @@ emoji=true
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
@@ -42,6 +44,10 @@ suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
[lints]
|
||||
sketchy-null-number=warn
|
||||
sketchy-null-mixed=warn
|
||||
@@ -53,6 +59,7 @@ unsafe-getters-setters=warn
|
||||
inexact-spread=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
deprecated-utility=error
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
@@ -64,4 +71,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.137.0
|
||||
^0.122.0
|
||||
|
||||
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -1,3 +1,4 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
||||
*.pbxproj -text
|
||||
|
||||
# specific for windows script files
|
||||
*.bat text eol=crlf
|
||||
|
||||
32
.github/ISSUE_TEMPLATE.md
vendored
32
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,32 +0,0 @@
|
||||
Per Mattermost guidelines, GitHub issues are for bug reports: <http://www.mattermost.org/filing-issues/>.
|
||||
|
||||
For troubleshooting see: http://forum.mattermost.org/.
|
||||
For feature proposals see: http://www.mattermost.org/feature-requests/
|
||||
|
||||
If you've found a bug--something appears unintentional--please follow these steps:
|
||||
|
||||
1. Confirm you’re filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
|
||||
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://about.mattermost.com/report-security-issue/).
|
||||
3. [File a new issue](https://github.com/mattermost/mattermost-mobile/issues/new) using the format below. Mattermost will confirm steps to reproduce and file in Jira, or ask for more details if there is trouble reproducing it. If there's already an existing bug in Jira, it will be linked back to the GitHub issue so you can track when it gets fixed.
|
||||
|
||||
#### Summary
|
||||
Bug report in one concise sentence
|
||||
|
||||
### Environment Information
|
||||
- Device Name:
|
||||
- OS Version:
|
||||
- Mattermost App Version:
|
||||
- Mattermost Server Version:
|
||||
|
||||
#### Steps to reproduce
|
||||
How can we reproduce the issue (what version are you using?)
|
||||
|
||||
#### Expected behavior
|
||||
Describe your issue in detail
|
||||
|
||||
#### Observed behavior (that appears unintentional)
|
||||
What did you see happen? Please include relevant error messages, screenshots and/or video recordings.
|
||||
|
||||
#### Possible fixes
|
||||
If you can, link to the line of code that might be responsible for the problem
|
||||
|
||||
61
.github/PULL_REQUEST_TEMPLATE.md
vendored
61
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,61 +0,0 @@
|
||||
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
|
||||
|
||||
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
|
||||
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
|
||||
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
|
||||
-->
|
||||
|
||||
#### Summary
|
||||
<!--
|
||||
A brief description of what this pull request does.
|
||||
-->
|
||||
|
||||
#### Ticket Link
|
||||
<!--
|
||||
If this pull request addresses a Help Wanted ticket or fixes a reported issue, please link the relevant GitHub issue, e.g.
|
||||
|
||||
Fixes https://github.com/mattermost/mattermost-mobile/issues/XXXXX
|
||||
|
||||
Otherwise, link the JIRA ticket.
|
||||
-->
|
||||
|
||||
#### Checklist
|
||||
<!--
|
||||
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
|
||||
-->
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
|
||||
#### Screenshots
|
||||
<!--
|
||||
If the PR includes UI changes, include screenshots/GIFs/Videos (for both iOS and Android if possible).
|
||||
-->
|
||||
|
||||
#### Release Note
|
||||
<!--
|
||||
Add a release note for each of the following conditions:
|
||||
|
||||
* New features and improvements, including behavioural changes, UI changes
|
||||
* Bug fixes and fixes of previous known issues
|
||||
* Deprecation warnings, breaking changes, or compatibility notes
|
||||
|
||||
If no release notes are required write NONE. Use past-tense. Newlines are stripped.
|
||||
|
||||
Example:
|
||||
|
||||
```release-note
|
||||
Added a new config setting ServiceSettings.FooBar. Added a new column Foo to the Users table.
|
||||
```
|
||||
|
||||
```release-note
|
||||
NONE
|
||||
```
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -95,7 +95,6 @@ coverage
|
||||
mattermost-license.txt
|
||||
*.mattermost-license
|
||||
detox/artifacts
|
||||
detox/detox_pixel_4_xl_api_30
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
sh ./scripts/pre-commit.sh
|
||||
63
NOTICE.txt
63
NOTICE.txt
@@ -694,6 +694,37 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## core-js
|
||||
|
||||
This product contains 'core-js' by Denis Pushkarev.
|
||||
|
||||
Modular standard library for JavaScript.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/zloirock/core-js
|
||||
|
||||
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## deep-equal
|
||||
|
||||
This product contains 'deep-equal' by James Halliday.
|
||||
@@ -2487,38 +2518,28 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## react-native-startup-time
|
||||
## react-native-status-bar-size
|
||||
|
||||
This product contains 'react-native-startup-time' by doomsower.
|
||||
This product contains 'react-native-status-bar-size' by Brent Vatne.
|
||||
|
||||
This module helps you to measure your app launch time.
|
||||
Watch and respond to changes in the iOS status bar height
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/doomsower/react-native-startup-time
|
||||
* https://github.com/jgkim/react-native-status-bar-size#readme
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
Note: An original license file for this dependency is not available. We determined the type of license based on the package registry entry for this project. The following text has been prepared using a template from the SPDX Workgroup (https://spdx.org) for this type of license.
|
||||
|
||||
Copyright (c) 2019 Konstantin Kuznetsov
|
||||
MIT License
|
||||
|
||||
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:
|
||||
Copyright (c) 2019 Brent Vatne
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
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 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.31.3)
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
||||
@@ -132,8 +132,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 360
|
||||
versionName "1.44.1"
|
||||
versionCode 349
|
||||
versionName "1.41.1"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -191,10 +191,6 @@ android {
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/*.so'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -4,10 +4,5 @@
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="">
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -51,6 +50,7 @@
|
||||
<data android:scheme="mmauthbeta" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
<service android:name=".NotificationDismissService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
Binary file not shown.
@@ -33,16 +33,14 @@ import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactMarker;
|
||||
import com.facebook.react.bridge.ReactMarkerConstants;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.module.model.ReactModuleInfo;
|
||||
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
|
||||
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
|
||||
public static MainApplication instance;
|
||||
|
||||
@@ -115,11 +113,6 @@ private final ReactNativeHost mReactNativeHost =
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new ReanimatedJSIModulePackage();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
@@ -139,6 +132,9 @@ private final ReactNativeHost mReactNativeHost =
|
||||
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
|
||||
// Uncomment to listen to react markers for build that has telemetry enabled
|
||||
// addReactMarkerListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,6 +195,38 @@ private final ReactNativeHost mReactNativeHost =
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addReactMarkerListener() {
|
||||
ReactMarker.addListener(new ReactMarker.MarkerListener() {
|
||||
@Override
|
||||
public void logMarker(ReactMarkerConstants name, @Nullable String tag, int instanceKey) {
|
||||
if (name.toString() == ReactMarkerConstants.RELOAD.toString()) {
|
||||
APP_START_TIME = System.currentTimeMillis();
|
||||
RELOAD = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_START.toString()) {
|
||||
PROCESS_PACKAGES_START = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.PROCESS_PACKAGES_END.toString()) {
|
||||
PROCESS_PACKAGES_END = System.currentTimeMillis();
|
||||
} else if (name.toString() == ReactMarkerConstants.CONTENT_APPEARED.toString()) {
|
||||
CONTENT_APPEARED = System.currentTimeMillis();
|
||||
ReactContext ctx = getRunningReactContext();
|
||||
|
||||
if (ctx != null) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
|
||||
map.putDouble("appReload", RELOAD);
|
||||
map.putDouble("appContentAppeared", CONTENT_APPEARED);
|
||||
|
||||
map.putDouble("processPackagesStart", PROCESS_PACKAGES_START);
|
||||
map.putDouble("processPackagesEnd", PROCESS_PACKAGES_END);
|
||||
|
||||
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
|
||||
emit("nativeMetrics", map);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads Flipper in React Native templates. Call this in the onCreate method with something like
|
||||
* initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
@@ -214,7 +242,7 @@ private final ReactNativeHost mReactNativeHost =
|
||||
We use reflection here to pick up the class that initializes Flipper,
|
||||
since Flipper library is not available in release mode
|
||||
*/
|
||||
Class<?> aClass = Class.forName("com.rn.ReactNativeFlipper");
|
||||
Class<?> aClass = Class.forName("com.rndiffapp.ReactNativeFlipper");
|
||||
aClass
|
||||
.getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
|
||||
.invoke(null, context, reactInstanceManager);
|
||||
|
||||
@@ -16,20 +16,12 @@ import com.mattermost.react_native_interface.KeysReadableArray;
|
||||
|
||||
public class MattermostCredentialsHelper {
|
||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
||||
static KeychainModule keychainModule;
|
||||
static AsyncStorageHelper asyncStorage;
|
||||
|
||||
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
|
||||
final KeychainModule keychainModule = new KeychainModule(context);
|
||||
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
|
||||
final ArrayList<String> keys = new ArrayList<String>(1);
|
||||
keys.add(CURRENT_SERVER_URL);
|
||||
|
||||
if (keychainModule == null) {
|
||||
keychainModule = new KeychainModule(context);
|
||||
}
|
||||
|
||||
if (asyncStorage == null) {
|
||||
asyncStorage = new AsyncStorageHelper(context);
|
||||
}
|
||||
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
|
||||
@Override
|
||||
public int size() {
|
||||
|
||||
@@ -3,9 +3,11 @@ package com.mattermost.share;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentResolver;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
@@ -13,18 +15,18 @@ import android.util.Log;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
|
||||
|
||||
public class RealPathUtil {
|
||||
public static String getRealPathFromURI(final Context context, final Uri uri) {
|
||||
|
||||
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
@@ -70,11 +72,7 @@ public class RealPathUtil {
|
||||
split[1]
|
||||
};
|
||||
|
||||
if (contentUri != null) {
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
} else {
|
||||
return getPathFromSavingTempFile(context, uri);
|
||||
}
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +107,6 @@ public class RealPathUtil {
|
||||
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
returnCursor.moveToFirst();
|
||||
fileName = sanitizeFilename(returnCursor.getString(nameIndex));
|
||||
returnCursor.close();
|
||||
|
||||
} catch (Exception e) {
|
||||
// just continue to get the filename with the last segment of the path
|
||||
@@ -117,7 +114,7 @@ public class RealPathUtil {
|
||||
|
||||
try {
|
||||
if (TextUtils.isEmpty(fileName)) {
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().trim());
|
||||
fileName = sanitizeFilename(uri.getLastPathSegment().toString().trim());
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +123,7 @@ public class RealPathUtil {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(uri.getPath());
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
@@ -232,7 +230,7 @@ public class RealPathUtil {
|
||||
|
||||
private static void deleteRecursive(File fileOrDirectory) {
|
||||
if (fileOrDirectory.isDirectory())
|
||||
for (File child : Objects.requireNonNull(fileOrDirectory.listFiles()))
|
||||
for (File child : fileOrDirectory.listFiles())
|
||||
deleteRecursive(child);
|
||||
|
||||
fileOrDirectory.delete();
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.json.JSONException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -46,7 +45,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
super(reactContext);
|
||||
mApplication = application;
|
||||
}
|
||||
|
||||
private File tempFolder;
|
||||
|
||||
@Override
|
||||
@@ -133,7 +131,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
String text = "";
|
||||
String type = "";
|
||||
String action = "";
|
||||
String extra = "";
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
@@ -142,21 +139,20 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
|
||||
if (type == null) {
|
||||
type = "";
|
||||
}
|
||||
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
|
||||
map.putString("value", extra);
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
|
||||
text = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
map.putString("value", text);
|
||||
map.putString("type", type);
|
||||
map.putBoolean("isString", true);
|
||||
items.pushMap(map);
|
||||
} else if (Intent.ACTION_SEND.equals(action)) {
|
||||
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (uri != null) {
|
||||
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
|
||||
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map.putString("value", text);
|
||||
|
||||
if (type.equals("image/*")) {
|
||||
type = "image/jpeg";
|
||||
@@ -165,16 +161,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
map.putString("type", type);
|
||||
map.putBoolean("isString", false);
|
||||
items.pushMap(map);
|
||||
}
|
||||
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
for (Uri uri : Objects.requireNonNull(uris)) {
|
||||
for (Uri uri : uris) {
|
||||
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
|
||||
map = Arguments.createMap();
|
||||
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
|
||||
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
|
||||
text = "file://" + filePath;
|
||||
map.putString("value", text);
|
||||
|
||||
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
|
||||
if (type != null) {
|
||||
if (type.equals("image/*")) {
|
||||
type = "image/jpeg";
|
||||
@@ -185,7 +182,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
type = "application/octet-stream";
|
||||
}
|
||||
map.putString("type", type);
|
||||
map.putBoolean("isString", false);
|
||||
items.pushMap(map);
|
||||
}
|
||||
}
|
||||
@@ -225,7 +221,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
for (int i = 0; i < files.size(); i++) {
|
||||
for(int i = 0 ; i < files.size() ; i++) {
|
||||
ReadableMap file = files.getMap(i);
|
||||
String filePath = file.getString("fullPath").replaceFirst("file://", "");
|
||||
File fileInfo = new File(filePath);
|
||||
@@ -249,7 +245,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
for (int i = 0; i < fileInfoArray.length(); i++) {
|
||||
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
|
||||
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
|
||||
file_ids.put(fileInfo.getString("id"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
buildToolsVersion = "29.0.2"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
@@ -10,7 +10,6 @@ buildscript {
|
||||
kotlinVersion = "1.3.61"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
ndkVersion = "21.1.6352462"
|
||||
|
||||
}
|
||||
repositories {
|
||||
@@ -20,7 +19,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
@@ -29,6 +28,19 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
afterEvaluate {
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.75.1
|
||||
FLIPPER_VERSION=0.37.0
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
|
||||
22
android/gradlew.bat
vendored
22
android/gradlew.bat
vendored
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,14 +64,28 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
|
||||
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {showModal} from './navigation';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeCallErrorResponse} from '@utils/apps';
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
|
||||
switch (responseType) {
|
||||
case AppCallResponseTypes.OK:
|
||||
return {data: res};
|
||||
case AppCallResponseTypes.ERROR:
|
||||
return {error: res};
|
||||
case AppCallResponseTypes.FORM: {
|
||||
if (!res.form) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.form.no_form',
|
||||
defaultMessage: 'Response type is `form`, but no form was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
const screen = EphemeralStore.getNavigationTopComponentId();
|
||||
if (type === AppCallTypes.SUBMIT && screen !== 'AppForm') {
|
||||
showAppForm(res.form, call, getTheme(getState()));
|
||||
}
|
||||
|
||||
return {data: res};
|
||||
}
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
if (!res.navigate_to_url) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.navigate.no_url',
|
||||
defaultMessage: 'Response type is `navigate`, but no url was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
if (type !== AppCallTypes.SUBMIT) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.navigate.no_submit',
|
||||
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
dispatch(handleGotoLocation(res.navigate_to_url, intl));
|
||||
|
||||
return {data: res};
|
||||
default: {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: responseType,
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error.message || intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_error',
|
||||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) => {
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
let submitButtons = [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: 'Submit',
|
||||
}];
|
||||
if (form.submit_buttons) {
|
||||
const options = form.fields.find((f) => f.name === form.submit_buttons)?.options;
|
||||
const newButtons = options?.map((o) => {
|
||||
return {
|
||||
id: 'submit-form_' + o.value,
|
||||
showAsAction: 'always',
|
||||
text: o.label,
|
||||
};
|
||||
});
|
||||
if (newButtons && newButtons.length > 0) {
|
||||
submitButtons = newButtons;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: submitButtons,
|
||||
},
|
||||
};
|
||||
|
||||
const passProps = {form, call};
|
||||
showModal('AppForm', form.title, passProps, options);
|
||||
};
|
||||
|
||||
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
post.channel_id,
|
||||
post.root_id || post.id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForChannel(response: AppCallResponse, message: string, channelID: string): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
channelID,
|
||||
'',
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForContext(response: AppCallResponse, message: string, context: AppContext): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
context.channel_id,
|
||||
context.root_id || context.post_id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForCommandArgs(response: AppCallResponse, message: string, args: CommandArgs): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
args.channel_id,
|
||||
args.root_id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,13 @@ export function connection(isOnline) {
|
||||
};
|
||||
}
|
||||
|
||||
export function setStatusBarHeight(height = 20) {
|
||||
return {
|
||||
type: DeviceTypes.STATUSBAR_HEIGHT_CHANGED,
|
||||
data: height,
|
||||
};
|
||||
}
|
||||
|
||||
export function setDeviceDimensions(height, width) {
|
||||
return {
|
||||
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
|
||||
@@ -44,4 +51,5 @@ export default {
|
||||
setDeviceDimensions,
|
||||
setDeviceOrientation,
|
||||
setDeviceAsTablet,
|
||||
setStatusBarHeight,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
@@ -10,7 +10,6 @@ import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmmiter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import {CHANNEL} from '@constants/screen';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Store from '@store/store';
|
||||
|
||||
@@ -34,8 +33,8 @@ export function resetToChannel(passProps = {}) {
|
||||
const stack = {
|
||||
children: [{
|
||||
component: {
|
||||
id: CHANNEL,
|
||||
name: CHANNEL,
|
||||
id: NavigationTypes.CHANNEL_SCREEN,
|
||||
name: NavigationTypes.CHANNEL_SCREEN,
|
||||
passProps,
|
||||
options: {
|
||||
layout: {
|
||||
@@ -364,20 +363,17 @@ export async function dismissModal(options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissAllModals(options) {
|
||||
export async function dismissAllModals(options = {}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
const modals = [...EphemeralStore.navigationModalStack];
|
||||
for await (const modal of modals) {
|
||||
await Navigation.dismissModal(modal, options);
|
||||
EphemeralStore.removeNavigationModal(modal);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await Navigation.dismissAllModals(options);
|
||||
EphemeralStore.clearNavigationModals();
|
||||
} catch (error) {
|
||||
// RNN returns a promise rejection if there are no modals to
|
||||
// dismiss. We'll do nothing in this case.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,7 +441,7 @@ export function closeMainSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(CHANNEL, {
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
left: {visible: false},
|
||||
},
|
||||
@@ -457,7 +453,7 @@ export function enableMainSideMenu(enabled, visible = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.mergeOptions(CHANNEL, {
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
left: {enabled, visible},
|
||||
},
|
||||
@@ -470,7 +466,7 @@ export function openSettingsSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(CHANNEL, {
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
right: {visible: true},
|
||||
},
|
||||
@@ -483,7 +479,7 @@ export function closeSettingsSideMenu() {
|
||||
}
|
||||
|
||||
Keyboard.dismiss();
|
||||
Navigation.mergeOptions(CHANNEL, {
|
||||
Navigation.mergeOptions(NavigationTypes.CHANNEL_SCREEN, {
|
||||
sideMenu: {
|
||||
right: {visible: false},
|
||||
},
|
||||
|
||||
@@ -17,23 +17,17 @@ import Store from '@store/store';
|
||||
import {NavigationTypes} from '@constants';
|
||||
|
||||
jest.unmock('@actions/navigation');
|
||||
jest.mock('@store/ephemeral_store', () => ({
|
||||
getNavigationTopComponentId: jest.fn(),
|
||||
clearNavigationComponents: jest.fn(),
|
||||
addNavigationModal: jest.fn(),
|
||||
hasModalsOpened: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
const store = mockStore(intitialState);
|
||||
Store.redux = store;
|
||||
|
||||
// Mock EphemeralStore add/remove modal
|
||||
const add = EphemeralStore.addNavigationModal;
|
||||
const remove = EphemeralStore.removeNavigationModal;
|
||||
EphemeralStore.removeNavigationModal = (componentId) => {
|
||||
remove(componentId);
|
||||
EphemeralStore.removeNavigationComponentId(componentId);
|
||||
};
|
||||
|
||||
EphemeralStore.addNavigationModal = (componentId) => {
|
||||
add(componentId);
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
};
|
||||
|
||||
describe('@actions/navigation', () => {
|
||||
const topComponentId = 'top-component-id';
|
||||
const name = 'name';
|
||||
@@ -45,16 +39,7 @@ describe('@actions/navigation', () => {
|
||||
const options = {
|
||||
testOption: 'test',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
EphemeralStore.clearNavigationModals();
|
||||
|
||||
// mock that we have a root screen
|
||||
EphemeralStore.addNavigationComponentId(topComponentId);
|
||||
});
|
||||
|
||||
// EphemeralStore.getNavigationTopComponentId.mockReturnValue(topComponentId);
|
||||
EphemeralStore.getNavigationTopComponentId.mockReturnValue(topComponentId);
|
||||
|
||||
test('resetToChannel should call Navigation.setRoot', () => {
|
||||
const setRoot = jest.spyOn(Navigation, 'setRoot');
|
||||
@@ -440,20 +425,15 @@ describe('@actions/navigation', () => {
|
||||
test('dismissModal should call Navigation.dismissModal', async () => {
|
||||
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
|
||||
|
||||
NavigationActions.showModal('First', 'First Modal', passProps, options);
|
||||
|
||||
await NavigationActions.dismissModal(options);
|
||||
expect(dismissModal).toHaveBeenCalledWith('First', options);
|
||||
expect(dismissModal).toHaveBeenCalledWith(topComponentId, options);
|
||||
});
|
||||
|
||||
test('dismissAllModals should call Navigation.dismissAllModals', async () => {
|
||||
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
|
||||
|
||||
NavigationActions.showModal('First', 'First Modal', passProps, options);
|
||||
NavigationActions.showModal('Second', 'Second Modal', passProps, options);
|
||||
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
|
||||
|
||||
await NavigationActions.dismissAllModals(options);
|
||||
expect(dismissModal).toHaveBeenCalledTimes(2);
|
||||
expect(dismissAllModals).toHaveBeenCalledWith(options);
|
||||
});
|
||||
|
||||
test('mergeNavigationOptions should call Navigation.mergeOptions', () => {
|
||||
@@ -513,15 +493,12 @@ describe('@actions/navigation', () => {
|
||||
});
|
||||
|
||||
test('dismissAllModalsAndPopToRoot should call Navigation.dismissAllModals, Navigation.popToRoot, and emit event', async () => {
|
||||
const dismissModal = jest.spyOn(Navigation, 'dismissModal');
|
||||
const dismissAllModals = jest.spyOn(Navigation, 'dismissAllModals');
|
||||
const popToRoot = jest.spyOn(Navigation, 'popToRoot');
|
||||
EventEmitter.emit = jest.fn();
|
||||
|
||||
NavigationActions.showModal('First', 'First Modal', passProps, options);
|
||||
NavigationActions.showModal('Second', 'Second Modal', passProps, options);
|
||||
|
||||
await NavigationActions.dismissAllModalsAndPopToRoot();
|
||||
expect(dismissModal).toHaveBeenCalledTimes(2);
|
||||
expect(dismissAllModals).toHaveBeenCalled();
|
||||
expect(popToRoot).toHaveBeenCalledWith(topComponentId);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith(NavigationTypes.NAVIGATION_DISMISS_AND_POP_TO_ROOT);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import {savePreferences} from '@mm-redux/actions/preferences';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {addUserToTeam, getTeamByName, removeUserFromTeam, selectTeam} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
@@ -36,11 +36,9 @@ import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/
|
||||
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import telemetry, {PERF_MARKERS} from '@telemetry';
|
||||
import telemetry from '@telemetry';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue, privateChannelJoinPrompt} from '@utils/channels';
|
||||
import {isPendingPost} from '@utils/general';
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
@@ -187,21 +185,12 @@ export function handleSelectChannel(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const dt = Date.now();
|
||||
const state = getState();
|
||||
const {currentUserId} = state.entities.users;
|
||||
const {channels, currentChannelId, myMembers} = state.entities.channels;
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
|
||||
if (channel) {
|
||||
let markerExtra;
|
||||
if (channel.display_name) {
|
||||
markerExtra = `Channel: ${channel.display_name}`;
|
||||
} else {
|
||||
markerExtra = `Channel: ${channel.type === General.DM_CHANNEL ? 'Direct Channel' : channel.name}`;
|
||||
}
|
||||
|
||||
telemetry.start([PERF_MARKERS.CHANNEL_RENDER], Date.now(), [markerExtra]);
|
||||
dispatch(loadPostsIfNecessaryWithRetry(channelId));
|
||||
|
||||
let previousChannelId = null;
|
||||
@@ -222,10 +211,6 @@ export function handleSelectChannel(channelId) {
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
|
||||
|
||||
if (appsEnabled(state)) {
|
||||
//TODO improve sync method
|
||||
dispatch(fetchAppBindings(currentUserId, channelId));
|
||||
}
|
||||
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
|
||||
}
|
||||
|
||||
@@ -247,7 +232,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler, i
|
||||
// Fallback to API response error, if any.
|
||||
if (teamError) {
|
||||
if (errorHandler) {
|
||||
errorHandler(intl);
|
||||
errorHandler();
|
||||
}
|
||||
return {error: teamError};
|
||||
}
|
||||
@@ -539,6 +524,12 @@ export function leaveChannel(channel, reset = false) {
|
||||
}
|
||||
|
||||
export function setChannelLoading(loading = true) {
|
||||
if (loading) {
|
||||
telemetry.start(['channel:loading']);
|
||||
} else {
|
||||
telemetry.end(['channel:loading']);
|
||||
}
|
||||
|
||||
return {
|
||||
type: ViewTypes.SET_CHANNEL_LOADER,
|
||||
loading,
|
||||
@@ -587,6 +578,9 @@ export function increasePostVisibility(channelId, postId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
telemetry.reset();
|
||||
telemetry.start(['posts:loading']);
|
||||
|
||||
dispatch({
|
||||
type: ViewTypes.LOADING_POSTS,
|
||||
data: true,
|
||||
@@ -617,6 +611,8 @@ export function increasePostVisibility(channelId, postId) {
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_LOAD_MORE_POSTS'));
|
||||
telemetry.end(['posts:loading']);
|
||||
telemetry.save();
|
||||
|
||||
return hasMorePost;
|
||||
};
|
||||
@@ -629,7 +625,7 @@ function setLoadMorePostsVisible(visible) {
|
||||
};
|
||||
}
|
||||
|
||||
function loadGroupData(isReconnect = false) {
|
||||
function loadGroupData() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const actions = [];
|
||||
@@ -662,10 +658,9 @@ function loadGroupData(isReconnect = false) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const getGroupsSince = isReconnect ? (state.websocket?.lastDisconnectAt || 0) : undefined;
|
||||
const [getAllGroupsAssociatedToChannelsInTeam, getGroups] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getAllGroupsAssociatedToChannelsInTeam(team.id, true),
|
||||
Client4.getGroups(false, 0, 0, getGroupsSince),
|
||||
Client4.getGroups(true, 0, 0),
|
||||
]);
|
||||
|
||||
if (getAllGroupsAssociatedToChannelsInTeam.groups) {
|
||||
@@ -711,11 +706,10 @@ function loadGroupData(isReconnect = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect = false) {
|
||||
export function loadChannelsForTeam(teamId, skipDispatch = false) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const lastConnectAt = state.websocket?.lastConnectAt || 0;
|
||||
const data = {
|
||||
sync: true,
|
||||
teamId,
|
||||
@@ -723,12 +717,13 @@ export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect =
|
||||
};
|
||||
|
||||
const actions = [];
|
||||
|
||||
if (currentUserId) {
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
try {
|
||||
console.log('Fetching channels attempt', (i + 1), teamId, 'include deleted since', lastConnectAt); //eslint-disable-line no-console
|
||||
console.log('Fetching channels attempt', teamId, (i + 1)); //eslint-disable-line no-console
|
||||
const [channels, channelMembers] = await Promise.all([ //eslint-disable-line no-await-in-loop
|
||||
Client4.getMyChannels(teamId, true, lastConnectAt),
|
||||
Client4.getMyChannels(teamId, true),
|
||||
Client4.getMyChannelMembers(teamId),
|
||||
]);
|
||||
|
||||
@@ -782,7 +777,7 @@ export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect =
|
||||
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
||||
}
|
||||
|
||||
dispatch(loadGroupData(isReconnect));
|
||||
dispatch(loadGroupData());
|
||||
}
|
||||
|
||||
return {data};
|
||||
|
||||
10
app/actions/views/client_upgrade.js
Normal file
10
app/actions/views/client_upgrade.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function setLastUpgradeCheck() {
|
||||
return {
|
||||
type: ViewTypes.SET_LAST_UPGRADE_CHECK,
|
||||
};
|
||||
}
|
||||
39
app/actions/views/command.js
Normal file
39
app/actions/views/command.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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) => {
|
||||
const state = getState();
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
const args = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
if (cmdLength < 0) {
|
||||
cmdLength = msg.length;
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data?.trigger_id) { //eslint-disable-line camelcase
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
return {data, error};
|
||||
};
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
|
||||
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
const args: CommandArgs = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
msg = filterEmDashForCommand(msg);
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
if (cmdLength < 0) {
|
||||
cmdLength = msg.length;
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
const appsAreEnabled = appsEnabled(state);
|
||||
if (appsAreEnabled) {
|
||||
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.root_id);
|
||||
if (parser.isAppCommand(msg)) {
|
||||
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
return {error: {message: errMessage}};
|
||||
};
|
||||
|
||||
if (!call) {
|
||||
return createErrorMessage(errorMessage!);
|
||||
}
|
||||
|
||||
const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
|
||||
if (res.error) {
|
||||
const errorResponse = res.error as AppCallResponse;
|
||||
return createErrorMessage(errorResponse.error || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
}));
|
||||
}
|
||||
const callResp = res.data as AppCallResponse;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {data: {}};
|
||||
default:
|
||||
return createErrorMessage(intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data?.trigger_id) { //eslint-disable-line camelcase
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
return {data, error};
|
||||
};
|
||||
}
|
||||
|
||||
const filterEmDashForCommand = (command: string): string => {
|
||||
return command.replace(/\u2014/g, '--');
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/common';
|
||||
import {ActionFunc, DispatchFunc, batchActions, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
|
||||
export function setCustomStatus(customStatus: UserCustomStatus): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const user = getCurrentUser(getState());
|
||||
if (!user.props) {
|
||||
user.props = {};
|
||||
}
|
||||
|
||||
const oldCustomStatus = user.props.customStatus;
|
||||
user.props.customStatus = JSON.stringify(customStatus);
|
||||
dispatch({type: UserTypes.RECEIVED_ME, data: user});
|
||||
|
||||
try {
|
||||
await Client4.updateCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
user.props.customStatus = oldCustomStatus;
|
||||
dispatch(batchActions([
|
||||
{type: UserTypes.RECEIVED_ME, data: user},
|
||||
logError(error),
|
||||
]));
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function unsetCustomStatus(): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
try {
|
||||
await Client4.unsetCustomStatus();
|
||||
} catch (error) {
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function removeRecentCustomStatus(customStatus: UserCustomStatus): ActionFunc {
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
try {
|
||||
await Client4.removeRecentCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
setCustomStatus,
|
||||
unsetCustomStatus,
|
||||
removeRecentCustomStatus,
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {EmojiTypes} from '@mm-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 '@client/rest';
|
||||
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';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {handleSuccessfulLogin} from 'app/actions/views/login';
|
||||
|
||||
|
||||
@@ -4,49 +4,43 @@
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Keyboard} from 'react-native';
|
||||
|
||||
import {dismissAllModals, showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {loadChannelsByTeamName} from '@actions/views/channel';
|
||||
import {selectFocusedPostId} from '@mm-redux/actions/posts';
|
||||
import {getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
import type {DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {permalinkBadTeam} from '@utils/general';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
|
||||
let showingPermalink = false;
|
||||
export let showingPermalink = false;
|
||||
|
||||
export function showPermalink(intl: typeof intlShape, teamName: string, postId: string, openAsPermalink = true) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
let name = teamName;
|
||||
if (!name) {
|
||||
name = getCurrentTeam(getState()).name;
|
||||
}
|
||||
|
||||
const loadTeam = await dispatch(loadChannelsByTeamName(name, permalinkBadTeam.bind(null, intl)));
|
||||
return async (dispatch: DispatchFunc) => {
|
||||
const loadTeam = await dispatch(loadChannelsByTeamName(teamName, permalinkBadTeam.bind(null, intl)));
|
||||
|
||||
if (!loadTeam.error) {
|
||||
Keyboard.dismiss();
|
||||
dispatch(selectFocusedPostId(postId));
|
||||
if (showingPermalink) {
|
||||
await dismissAllModals();
|
||||
|
||||
if (!showingPermalink) {
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
onClose: () => {
|
||||
dispatch(closePermalink());
|
||||
},
|
||||
teamName,
|
||||
};
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
};
|
||||
|
||||
showingPermalink = true;
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
|
||||
const screen = 'Permalink';
|
||||
const passProps = {
|
||||
isPermalink: openAsPermalink,
|
||||
teamName,
|
||||
};
|
||||
|
||||
const options = {
|
||||
layout: {
|
||||
componentBackgroundColor: changeOpacity('#000', 0.2),
|
||||
},
|
||||
};
|
||||
|
||||
showingPermalink = true;
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
receivedPostsSince,
|
||||
receivedPostsInThread,
|
||||
} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
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';
|
||||
@@ -28,28 +28,6 @@ import {getChannelSinceValue} from '@utils/channels';
|
||||
|
||||
import {getEmojisInPosts} from './emoji';
|
||||
|
||||
export function sendEphemeralPost(message, channelId = '', parentId = '', userId = '0') {
|
||||
return async (dispatch, getState) => {
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
id: generateId(),
|
||||
user_id: userId,
|
||||
channel_id: channelId || getCurrentChannelId(getState()),
|
||||
message,
|
||||
type: Posts.POST_TYPES.EPHEMERAL,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: parentId,
|
||||
parent_id: parentId,
|
||||
props: {},
|
||||
};
|
||||
|
||||
dispatch(receivedNewPost(post));
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
||||
return async (dispatch) => {
|
||||
const timestamp = Date.now();
|
||||
@@ -73,14 +51,13 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
};
|
||||
}
|
||||
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options, getDynamicOptions) {
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options) {
|
||||
return {
|
||||
type: ViewTypes.SELECTED_ACTION_MENU,
|
||||
data: {
|
||||
dataSource,
|
||||
onSelect,
|
||||
options,
|
||||
getDynamicOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {PostTypes, UserTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {NavigationTypes, ViewTypes} from '@constants';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {fetchMyChannelsAndMembers, getChannelAndMyMember} 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 '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import initialState from '@store/initial_state';
|
||||
@@ -179,6 +180,14 @@ export function createPostForNotificationReply(post) {
|
||||
};
|
||||
}
|
||||
|
||||
export function recordLoadTime(screenName, category) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentUserId} = getState().entities.users;
|
||||
|
||||
analytics.recordTime(screenName, category, currentUserId);
|
||||
};
|
||||
}
|
||||
|
||||
export function setDeepLinkURL(url) {
|
||||
return {
|
||||
type: ViewTypes.SET_DEEP_LINK_URL,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 '@client/rest';
|
||||
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';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
export function handleRefreshAppsBindings() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
if (appsEnabled(state)) {
|
||||
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import configureMockStore from 'redux-mock-store';
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as TeamActions from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {markChannelAsRead} from '@mm-redux/actions/channels';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {loadChannelsForTeam, setChannelRetryFailed} from '@actions/views/channel';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getPostsSince} from '@actions/views/post';
|
||||
import {loadMe} from '@actions/views/user';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, PreferenceTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
@@ -46,7 +46,6 @@ import {handleLeaveTeamEvent, handleUpdateTeamEvent, handleTeamAddedEvent} from
|
||||
import {handleStatusChangedEvent, handleUserAddedEvent, handleUserRemovedEvent, handleUserRoleUpdated, handleUserUpdatedEvent} from './users';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {handleRefreshAppsBindings} from './apps';
|
||||
|
||||
export function init(additionalOptions: any = {}) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
@@ -57,7 +56,6 @@ export function init(additionalOptions: any = {}) {
|
||||
connUrl += `${Client4.getUrlVersion()}/websocket`;
|
||||
websocketClient.setFirstConnectCallback(() => dispatch(handleFirstConnect()));
|
||||
websocketClient.setEventCallback((evt: WebSocketMessage) => dispatch(handleEvent(evt)));
|
||||
websocketClient.setMissedEventsCallback(() => dispatch(doMissedEvents()));
|
||||
websocketClient.setReconnectCallback(() => dispatch(handleReconnect()));
|
||||
websocketClient.setCloseCallback((connectFailCount: number) => dispatch(handleClose(connectFailCount)));
|
||||
|
||||
@@ -82,19 +80,15 @@ export function close(shouldReconnect = false): GenericAction {
|
||||
};
|
||||
}
|
||||
|
||||
function wsConnected(timestamp = Date.now()) {
|
||||
return {
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function doFirstConnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [wsConnected(now)];
|
||||
const actions: Array<GenericAction> = [{
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
}];
|
||||
|
||||
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
@@ -117,14 +111,6 @@ export function doFirstConnect(now: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export function doMissedEvents() {
|
||||
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
|
||||
dispatch(wsConnected());
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function doReconnect(now: number) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
@@ -135,10 +121,11 @@ export function doReconnect(now: number) {
|
||||
const {lastDisconnectAt} = state.websocket;
|
||||
const actions: Array<GenericAction> = [];
|
||||
|
||||
dispatch(batchActions([
|
||||
wsConnected(now),
|
||||
setChannelRetryFailed(false),
|
||||
], 'BATCH_WS_SUCCESS'));
|
||||
dispatch({
|
||||
type: GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
timestamp: now,
|
||||
data: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
@@ -151,9 +138,6 @@ export function doReconnect(now: number) {
|
||||
}
|
||||
|
||||
actions.push({
|
||||
type: UserTypes.RECEIVED_ME,
|
||||
data: me.user,
|
||||
}, {
|
||||
type: PreferenceTypes.RECEIVED_ALL_PREFERENCES,
|
||||
data: me.preferences,
|
||||
}, {
|
||||
@@ -170,7 +154,7 @@ export function doReconnect(now: number) {
|
||||
const currentTeamMembership = me.teamMembers.find((tm: TeamMembership) => tm.team_id === currentTeamId && tm.delete_at === 0);
|
||||
|
||||
if (currentTeamMembership) {
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true, true));
|
||||
const {data: myData}: any = await dispatch(loadChannelsForTeam(currentTeamId, true));
|
||||
|
||||
if (myData?.channels && myData?.channelMembers) {
|
||||
actions.push({
|
||||
@@ -288,16 +272,13 @@ export function handleUserTypingEvent(msg: WebSocketMessage) {
|
||||
}
|
||||
|
||||
function handleFirstConnect() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
return (dispatch: DispatchFunc) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (reconnect && config?.EnableReliableWebSockets !== 'true') {
|
||||
if (reconnect) {
|
||||
reconnect = false;
|
||||
return dispatch(doReconnect(now));
|
||||
}
|
||||
|
||||
return dispatch(doFirstConnect(now));
|
||||
};
|
||||
}
|
||||
@@ -396,9 +377,6 @@ function handleEvent(msg: WebSocketMessage) {
|
||||
return dispatch(handleOpenDialogEvent(msg));
|
||||
case WebsocketEvents.RECEIVED_GROUP:
|
||||
return dispatch(handleGroupUpdatedEvent(msg));
|
||||
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS: {
|
||||
return dispatch(handleRefreshAppsBindings());
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as PostActions from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts} from '@mm-redux/constants';
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {RoleTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
|
||||
@@ -5,7 +5,7 @@ import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getMe} from '@actions/views/user';
|
||||
import {ChannelTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getCurrentChannelId, getChannelMembersInChannels} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -171,7 +171,7 @@ export function handleUserRoleUpdated(msg: WebSocketMessage) {
|
||||
|
||||
dispatch({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data,
|
||||
data: data.roles,
|
||||
});
|
||||
} catch {
|
||||
// do nothing
|
||||
|
||||
@@ -9,9 +9,9 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {GeneralTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts, RequestStatus} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
@@ -30,7 +30,7 @@ const mockConfigRequest = (config = {}) => {
|
||||
|
||||
const mockChanelsRequest = (teamId, channels = []) => {
|
||||
nock(Client4.getUserRoute('me')).
|
||||
get(`/teams/${teamId}/channels?include_deleted=true&last_delete_at=0`).
|
||||
get(`/teams/${teamId}/channels?include_deleted=true`).
|
||||
reply(200, channels);
|
||||
};
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
'BATCH_WS_SUCCESS',
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
@@ -215,7 +215,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
'BATCH_WS_SUCCESS',
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
@@ -260,7 +260,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
'BATCH_WS_SUCCESS',
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
'BATCH_WS_RECONNECT',
|
||||
@@ -304,7 +304,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
'BATCH_WS_SUCCESS',
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
const expectedMissingActions = [
|
||||
@@ -337,7 +337,7 @@ describe('Actions.Websocket doReconnect', () => {
|
||||
const testStore = await mockStore(state);
|
||||
const timestamp = 1000;
|
||||
const expectedActions = [
|
||||
'BATCH_WS_SUCCESS',
|
||||
GeneralTypes.WEBSOCKET_SUCCESS,
|
||||
'BATCH_WS_LEAVE_TEAM',
|
||||
'BATCH_WS_RECONNECT',
|
||||
];
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
export interface ClientAppsMix {
|
||||
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
|
||||
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
|
||||
}
|
||||
|
||||
const ClientApps = (superclass: any) => class extends superclass {
|
||||
executeAppCall = async (call: AppCallRequest, type: AppCallType) => {
|
||||
const callCopy = {
|
||||
...call,
|
||||
path: `${call.path}/${type}`,
|
||||
context: {
|
||||
...call.context,
|
||||
user_agent: 'mobile',
|
||||
},
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getAppsProxyRoute()}/api/v1/call`,
|
||||
{method: 'post', body: JSON.stringify(callCopy)},
|
||||
);
|
||||
}
|
||||
|
||||
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
|
||||
const params = {
|
||||
user_id: userID,
|
||||
channel_id: channelID,
|
||||
team_id: teamID,
|
||||
user_agent: 'mobile',
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientApps;
|
||||
@@ -1,377 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {RNFetchBlobFetchRepsonse} from 'rn-fetch-blob';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Options} from '@mm-redux/types/client4';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
import ClientError from './error';
|
||||
|
||||
export default class ClientBase {
|
||||
clusterId = '';
|
||||
csrf = '';
|
||||
defaultHeaders: {[x: string]: string} = {};
|
||||
diagnosticId = '';
|
||||
enableLogging = false;
|
||||
includeCookies = true;
|
||||
logToConsole = false;
|
||||
managedConfig: any = null;
|
||||
serverVersion = '';
|
||||
token = '';
|
||||
translations = {
|
||||
connectionError: 'There appears to be a problem with your internet connection.',
|
||||
unknownError: 'We received an unexpected status code from the server.',
|
||||
};
|
||||
userAgent: string|null = null;
|
||||
url = '';
|
||||
urlVersion = '/api/v4';
|
||||
|
||||
getAbsoluteUrl(baseUrl: string) {
|
||||
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) {
|
||||
return baseUrl;
|
||||
}
|
||||
return this.getUrl() + baseUrl;
|
||||
}
|
||||
|
||||
getOptions(options: Options) {
|
||||
const newOptions: Options = {...options};
|
||||
|
||||
const headers: {[x: string]: string} = {
|
||||
[ClientConstants.HEADER_REQUESTED_WITH]: 'XMLHttpRequest',
|
||||
...this.defaultHeaders,
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${this.token}`;
|
||||
}
|
||||
|
||||
const csrfToken = this.csrf || '';
|
||||
if (options.method && options.method.toLowerCase() !== 'get' && csrfToken) {
|
||||
headers[ClientConstants.HEADER_X_CSRF_TOKEN] = csrfToken;
|
||||
}
|
||||
|
||||
if (this.includeCookies) {
|
||||
newOptions.credentials = 'include';
|
||||
}
|
||||
|
||||
if (this.userAgent) {
|
||||
headers[ClientConstants.HEADER_USER_AGENT] = this.userAgent;
|
||||
}
|
||||
|
||||
if (newOptions.headers) {
|
||||
Object.assign(headers, newOptions.headers);
|
||||
}
|
||||
|
||||
return {
|
||||
...newOptions,
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
getServerVersion() {
|
||||
return this.serverVersion;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
getUrlVersion() {
|
||||
return this.urlVersion;
|
||||
}
|
||||
|
||||
getWebSocketUrl = () => {
|
||||
return `${this.getBaseRoute()}/websocket`;
|
||||
}
|
||||
|
||||
setAcceptLanguage(locale: string) {
|
||||
this.defaultHeaders['Accept-Language'] = locale;
|
||||
}
|
||||
|
||||
setCSRF(csrfToken: string) {
|
||||
this.csrf = csrfToken;
|
||||
}
|
||||
|
||||
setDiagnosticId(diagnosticId: string) {
|
||||
this.diagnosticId = diagnosticId;
|
||||
}
|
||||
|
||||
setEnableLogging(enable: boolean) {
|
||||
this.enableLogging = enable;
|
||||
}
|
||||
|
||||
setIncludeCookies(include: boolean) {
|
||||
this.includeCookies = include;
|
||||
}
|
||||
|
||||
setManagedConfig(config: any) {
|
||||
this.managedConfig = config;
|
||||
}
|
||||
|
||||
setUserAgent(userAgent: string) {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
setUrl(url: string) {
|
||||
this.url = url.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// Routes
|
||||
getBaseRoute() {
|
||||
return `${this.url}${this.urlVersion}`;
|
||||
}
|
||||
|
||||
getUsersRoute() {
|
||||
return `${this.getBaseRoute()}/users`;
|
||||
}
|
||||
|
||||
getUserRoute(userId: string) {
|
||||
return `${this.getUsersRoute()}/${userId}`;
|
||||
}
|
||||
|
||||
getTeamsRoute() {
|
||||
return `${this.getBaseRoute()}/teams`;
|
||||
}
|
||||
|
||||
getTeamRoute(teamId: string) {
|
||||
return `${this.getTeamsRoute()}/${teamId}`;
|
||||
}
|
||||
|
||||
getTeamNameRoute(teamName: string) {
|
||||
return `${this.getTeamsRoute()}/name/${teamName}`;
|
||||
}
|
||||
|
||||
getTeamMembersRoute(teamId: string) {
|
||||
return `${this.getTeamRoute(teamId)}/members`;
|
||||
}
|
||||
|
||||
getTeamMemberRoute(teamId: string, userId: string) {
|
||||
return `${this.getTeamMembersRoute(teamId)}/${userId}`;
|
||||
}
|
||||
|
||||
getChannelsRoute() {
|
||||
return `${this.getBaseRoute()}/channels`;
|
||||
}
|
||||
|
||||
getChannelRoute(channelId: string) {
|
||||
return `${this.getChannelsRoute()}/${channelId}`;
|
||||
}
|
||||
|
||||
getChannelMembersRoute(channelId: string) {
|
||||
return `${this.getChannelRoute(channelId)}/members`;
|
||||
}
|
||||
|
||||
getChannelMemberRoute(channelId: string, userId: string) {
|
||||
return `${this.getChannelMembersRoute(channelId)}/${userId}`;
|
||||
}
|
||||
|
||||
getPostsRoute() {
|
||||
return `${this.getBaseRoute()}/posts`;
|
||||
}
|
||||
|
||||
getPostRoute(postId: string) {
|
||||
return `${this.getPostsRoute()}/${postId}`;
|
||||
}
|
||||
|
||||
getSharedChannelsRoute() {
|
||||
return `${this.getBaseRoute()}/sharedchannels`;
|
||||
}
|
||||
|
||||
getReactionsRoute() {
|
||||
return `${this.getBaseRoute()}/reactions`;
|
||||
}
|
||||
|
||||
getCommandsRoute() {
|
||||
return `${this.getBaseRoute()}/commands`;
|
||||
}
|
||||
|
||||
getFilesRoute() {
|
||||
return `${this.getBaseRoute()}/files`;
|
||||
}
|
||||
|
||||
getFileRoute(fileId: string) {
|
||||
return `${this.getFilesRoute()}/${fileId}`;
|
||||
}
|
||||
|
||||
getPreferencesRoute(userId: string) {
|
||||
return `${this.getUserRoute(userId)}/preferences`;
|
||||
}
|
||||
|
||||
getIncomingHooksRoute() {
|
||||
return `${this.getBaseRoute()}/hooks/incoming`;
|
||||
}
|
||||
|
||||
getIncomingHookRoute(hookId: string) {
|
||||
return `${this.getBaseRoute()}/hooks/incoming/${hookId}`;
|
||||
}
|
||||
|
||||
getOutgoingHooksRoute() {
|
||||
return `${this.getBaseRoute()}/hooks/outgoing`;
|
||||
}
|
||||
|
||||
getOutgoingHookRoute(hookId: string) {
|
||||
return `${this.getBaseRoute()}/hooks/outgoing/${hookId}`;
|
||||
}
|
||||
|
||||
getOAuthRoute() {
|
||||
return `${this.url}/oauth`;
|
||||
}
|
||||
|
||||
getOAuthAppsRoute() {
|
||||
return `${this.getBaseRoute()}/oauth/apps`;
|
||||
}
|
||||
|
||||
getOAuthAppRoute(appId: string) {
|
||||
return `${this.getOAuthAppsRoute()}/${appId}`;
|
||||
}
|
||||
|
||||
getEmojisRoute() {
|
||||
return `${this.getBaseRoute()}/emoji`;
|
||||
}
|
||||
|
||||
getEmojiRoute(emojiId: string) {
|
||||
return `${this.getEmojisRoute()}/${emojiId}`;
|
||||
}
|
||||
|
||||
getBrandRoute() {
|
||||
return `${this.getBaseRoute()}/brand`;
|
||||
}
|
||||
|
||||
getBrandImageUrl(timestamp: string) {
|
||||
return `${this.getBrandRoute()}/image?t=${timestamp}`;
|
||||
}
|
||||
|
||||
getDataRetentionRoute() {
|
||||
return `${this.getBaseRoute()}/data_retention`;
|
||||
}
|
||||
|
||||
getRolesRoute() {
|
||||
return `${this.getBaseRoute()}/roles`;
|
||||
}
|
||||
|
||||
getTimezonesRoute() {
|
||||
return `${this.getBaseRoute()}/system/timezones`;
|
||||
}
|
||||
|
||||
getRedirectLocationRoute() {
|
||||
return `${this.getBaseRoute()}/redirect_location`;
|
||||
}
|
||||
|
||||
getBotsRoute() {
|
||||
return `${this.getBaseRoute()}/bots`;
|
||||
}
|
||||
|
||||
getBotRoute(botUserId: string) {
|
||||
return `${this.getBotsRoute()}/${botUserId}`;
|
||||
}
|
||||
|
||||
getAppsProxyRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
|
||||
const serverUrl = this.getUrl();
|
||||
const parsed = urlParse(url);
|
||||
const {redirects} = response.rnfbRespInfo;
|
||||
if (redirects) {
|
||||
const redirectUrl = urlParse(redirects[redirects.length - 1]);
|
||||
|
||||
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
|
||||
this.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
doFetch = async (url: string, options: Options) => {
|
||||
const {data} = await this.doFetchWithResponse(url, options);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
doFetchWithResponse = async (url: string, options: Options) => {
|
||||
const response = await fetch(url, this.getOptions(options));
|
||||
const headers = parseAndMergeNestedHeaders(response.headers);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: 'Received invalid response from the server.',
|
||||
intl: {
|
||||
id: 'mobile.request.invalid_response',
|
||||
defaultMessage: 'Received invalid response from the server.',
|
||||
},
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
if (headers.has(ClientConstants.HEADER_X_VERSION_ID) && !headers.get('Cache-Control')) {
|
||||
const serverVersion = headers.get(ClientConstants.HEADER_X_VERSION_ID);
|
||||
if (serverVersion && this.serverVersion !== serverVersion) {
|
||||
this.serverVersion = serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (headers.has(ClientConstants.HEADER_X_CLUSTER_ID)) {
|
||||
const clusterId = headers.get(ClientConstants.HEADER_X_CLUSTER_ID);
|
||||
if (clusterId && this.clusterId !== clusterId) {
|
||||
this.clusterId = clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
response,
|
||||
headers,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
const msg = data.message || '';
|
||||
|
||||
if (this.logToConsole) {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: msg,
|
||||
server_error_id: data.id,
|
||||
status_code: data.status_code,
|
||||
url,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function parseAndMergeNestedHeaders(originalHeaders: any) {
|
||||
const headers = new Map();
|
||||
let nestedHeaders = new Map();
|
||||
originalHeaders.forEach((val: string, key: string) => {
|
||||
const capitalizedKey = key.replace(/\b[a-z]/g, (l) => l.toUpperCase());
|
||||
let realVal = val;
|
||||
if (val && val.match(/\n\S+:\s\S+/)) {
|
||||
const nestedHeaderStrings = val.split('\n');
|
||||
realVal = nestedHeaderStrings.shift() as string;
|
||||
const moreNestedHeaders = new Map(
|
||||
nestedHeaderStrings.map((h: any) => h.split(/:\s/)),
|
||||
);
|
||||
nestedHeaders = new Map([...nestedHeaders, ...moreNestedHeaders]);
|
||||
}
|
||||
headers.set(capitalizedKey, realVal);
|
||||
});
|
||||
return new Map([...headers, ...nestedHeaders]);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import type {Bot} from '@mm-redux/types/bots';
|
||||
|
||||
export interface ClientBotsMix {
|
||||
getBot: (botUserId: string) => Promise<Bot>;
|
||||
getBots: (page?: number, perPage?: number) => Promise<Bot[]>;
|
||||
getBotsIncludeDeleted: (page?: number, perPage?: number) => Promise<Bot[]>;
|
||||
}
|
||||
|
||||
const PER_PAGE_DEFAULT = 60;
|
||||
|
||||
const ClientBots = (superclass: any) => class extends superclass {
|
||||
getBot = async (botUserId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotRoute(botUserId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientBots;
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Channel, ChannelMemberCountByGroup, ChannelMembership, ChannelNotifyProps, ChannelStats} from '@mm-redux/types/channels';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientChannelsMix {
|
||||
getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise<any>;
|
||||
createChannel: (channel: Channel) => Promise<Channel>;
|
||||
createDirectChannel: (userIds: string[]) => Promise<Channel>;
|
||||
createGroupChannel: (userIds: string[]) => Promise<Channel>;
|
||||
deleteChannel: (channelId: string) => Promise<any>;
|
||||
unarchiveChannel: (channelId: string) => Promise<Channel>;
|
||||
updateChannel: (channel: Channel) => Promise<Channel>;
|
||||
convertChannelToPrivate: (channelId: string) => Promise<Channel>;
|
||||
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
|
||||
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
|
||||
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => Promise<any>;
|
||||
getChannel: (channelId: string) => Promise<Channel>;
|
||||
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number) => Promise<Channel[]>;
|
||||
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
|
||||
getMyChannelMembers: (teamId: string) => Promise<ChannelMembership[]>;
|
||||
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
|
||||
getChannelTimezones: (channelId: string) => Promise<string[]>;
|
||||
getChannelMember: (channelId: string, userId: string) => Promise<ChannelMembership>;
|
||||
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
|
||||
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
|
||||
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
|
||||
getChannelStats: (channelId: string) => Promise<ChannelStats>;
|
||||
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
|
||||
viewMyChannel: (channelId: string, prevChannelId?: string) => Promise<any>;
|
||||
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
|
||||
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
|
||||
}
|
||||
|
||||
const ClientChannels = (superclass: any) => class extends superclass {
|
||||
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
|
||||
const queryData = {
|
||||
page,
|
||||
per_page: perPage,
|
||||
not_associated_to_group: notAssociatedToGroup,
|
||||
exclude_default_channels: excludeDefaultChannels,
|
||||
include_total_count: includeTotalCount,
|
||||
};
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}${buildQueryString(queryData)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
createChannel = async (channel: Channel) => {
|
||||
analytics.trackAPI('api_channels_create', {team_id: channel.team_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(channel)},
|
||||
);
|
||||
};
|
||||
|
||||
createDirectChannel = async (userIds: string[]) => {
|
||||
analytics.trackAPI('api_channels_create_direct');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/direct`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
createGroupChannel = async (userIds: string[]) => {
|
||||
analytics.trackAPI('api_channels_create_group');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/group`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_delete', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
unarchiveChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/restore`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannel = async (channel: Channel) => {
|
||||
analytics.trackAPI('api_channels_update', {channel_id: channel.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channel.id)}`,
|
||||
{method: 'put', body: JSON.stringify(channel)},
|
||||
);
|
||||
};
|
||||
|
||||
convertChannelToPrivate = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/convert`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelPrivacy = async (channelId: string, privacy: any) => {
|
||||
analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/privacy`,
|
||||
{method: 'put', body: JSON.stringify({privacy})},
|
||||
);
|
||||
};
|
||||
|
||||
patchChannel = async (channelId: string, channelPatch: Partial<Channel>) => {
|
||||
analytics.trackAPI('api_channels_patch', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(channelPatch)},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => {
|
||||
analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`,
|
||||
{method: 'put', body: JSON.stringify(props)},
|
||||
);
|
||||
};
|
||||
|
||||
getChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channel_get', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelByName = async (teamId: string, channelName: string, includeDeleted = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => {
|
||||
analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, lastDeleteAt = 0) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
|
||||
include_deleted: includeDeleted,
|
||||
last_delete_at: lastDeleteAt,
|
||||
})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannelMember = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, 'me')}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannelMembers = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels/members`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMembers = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelTimezones = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/timezones`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMember = async (channelId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMembersByIds = async (channelId: string, userIds: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}/ids`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
addToChannel = async (userId: string, channelId: string, postRootId = '') => {
|
||||
analytics.trackAPI('api_channels_add_member', {channel_id: channelId});
|
||||
|
||||
const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId};
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}`,
|
||||
{method: 'post', body: JSON.stringify(member)},
|
||||
);
|
||||
};
|
||||
|
||||
removeFromChannel = async (userId: string, channelId: string) => {
|
||||
analytics.trackAPI('api_channels_remove_member', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, userId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelStats = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/stats`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMemberCountsByGroup = async (channelId: string, includeTimezones: boolean) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/member_counts_by_group?include_timezones=${includeTimezones}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
viewMyChannel = async (channelId: string, prevChannelId?: string) => {
|
||||
const data = {channel_id: channelId, prev_channel_id: prevChannelId};
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/members/me/view`,
|
||||
{method: 'post', body: JSON.stringify(data)},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteChannels = async (teamId: string, name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteChannelsForSearch = async (teamId: string, name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search_autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
searchChannels = async (teamId: string, term: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search`,
|
||||
{method: 'post', body: JSON.stringify({term})},
|
||||
);
|
||||
};
|
||||
|
||||
searchArchivedChannels = async (teamId: string, term: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search_archived`,
|
||||
{method: 'post', body: JSON.stringify({term})},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientChannels;
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const HEADER_AUTH = 'Authorization';
|
||||
export const HEADER_BEARER = 'BEARER';
|
||||
export const HEADER_REQUESTED_WITH = 'X-Requested-With';
|
||||
export const HEADER_USER_AGENT = 'User-Agent';
|
||||
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
|
||||
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
|
||||
export const HEADER_TOKEN = 'Token';
|
||||
export const HEADER_X_VERSION_ID = 'X-Version-Id';
|
||||
export const DEFAULT_LIMIT_BEFORE = 30;
|
||||
export const DEFAULT_LIMIT_AFTER = 30;
|
||||
export const PER_PAGE_DEFAULT = 60;
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import FormData from 'form-data';
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {CustomEmoji} from '@mm-redux/types/emojis';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientEmojisMix {
|
||||
createCustomEmoji: (emoji: CustomEmoji, imageData: any) => Promise<CustomEmoji>;
|
||||
getCustomEmoji: (id: string) => Promise<CustomEmoji>;
|
||||
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
|
||||
getCustomEmojis: (page?: number, perPage?: number, sort?: string) => Promise<CustomEmoji[]>;
|
||||
deleteCustomEmoji: (emojiId: string) => Promise<any>;
|
||||
getSystemEmojiImageUrl: (filename: string) => string;
|
||||
getCustomEmojiImageUrl: (id: string) => string;
|
||||
searchCustomEmoji: (term: string, options?: Record<string, any>) => Promise<CustomEmoji[]>;
|
||||
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
|
||||
}
|
||||
|
||||
const ClientEmojis = (superclass: any) => class extends superclass {
|
||||
createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => {
|
||||
analytics.trackAPI('api_emoji_custom_add');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageData);
|
||||
formData.append('emoji', JSON.stringify(emoji));
|
||||
const request: any = {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
};
|
||||
|
||||
if (formData.getBoundary) {
|
||||
request.headers = {
|
||||
'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}`,
|
||||
request,
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmoji = async (id: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/${id}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmojiByName = async (name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/name/${name}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmojis = async (page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}${buildQueryString({page, per_page: perPage, sort})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
deleteCustomEmoji = async (emojiId: string) => {
|
||||
analytics.trackAPI('api_emoji_custom_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getEmojiRoute(emojiId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getSystemEmojiImageUrl = (filename: string) => {
|
||||
return `${this.url}/static/emoji/${filename}.png`;
|
||||
};
|
||||
|
||||
getCustomEmojiImageUrl = (id: string) => {
|
||||
return `${this.getEmojiRoute(id)}/image`;
|
||||
};
|
||||
|
||||
searchCustomEmoji = async (term: string, options = {}) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/search`,
|
||||
{method: 'post', body: JSON.stringify({term, ...options})},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteCustomEmoji = async (name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientEmojis;
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {cleanUrlForLogging} from '@mm-redux/utils/sentry';
|
||||
|
||||
export default class ClientError extends Error {
|
||||
url: string;
|
||||
intl: { defaultMessage: string; id: string } | { defaultMessage: string; id: string } | { id: string; defaultMessage: string; values: any } | { id: string; defaultMessage: string };
|
||||
server_error_id: any;
|
||||
status_code: any;
|
||||
details: Error;
|
||||
constructor(baseUrl: string, data: any) {
|
||||
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
|
||||
|
||||
this.message = data.message;
|
||||
this.url = data.url;
|
||||
this.intl = data.intl;
|
||||
this.server_error_id = data.server_error_id;
|
||||
this.status_code = data.status_code;
|
||||
this.details = data.details;
|
||||
|
||||
// Ensure message is treated as a property of this class when object spreading. Without this,
|
||||
// copying the object by using `{...error}` would not include the message.
|
||||
Object.defineProperty(this, 'message', {enumerable: true});
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientFilesMix {
|
||||
getFileUrl: (fileId: string, timestamp: number) => string;
|
||||
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePreviewUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePublicLink: (fileId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientFiles = (superclass: any) => class extends superclass {
|
||||
getFileUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFileThumbnailUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}/thumbnail`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFilePreviewUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}/preview`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFilePublicLink = async (fileId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getFileRoute(fileId)}/link`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientFiles;
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Config} from '@mm-redux/types/config';
|
||||
import {Role} from '@mm-redux/types/roles';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import ClientError from './error';
|
||||
|
||||
export interface ClientGeneralMix {
|
||||
getOpenGraphMetadata: (url: string) => Promise<any>;
|
||||
ping: () => Promise<any>;
|
||||
logClientError: (message: string, level?: string) => Promise<any>;
|
||||
getClientConfigOld: () => Promise<Config>;
|
||||
getClientLicenseOld: () => Promise<any>;
|
||||
getTimezones: () => Promise<string[]>;
|
||||
getDataRetentionPolicy: () => Promise<any>;
|
||||
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
|
||||
getRedirectLocation: (urlParam: string) => Promise<Dictionary<string>>;
|
||||
}
|
||||
|
||||
const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
getOpenGraphMetadata = async (url: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/opengraph`,
|
||||
{method: 'post', body: JSON.stringify({url})},
|
||||
);
|
||||
};
|
||||
|
||||
ping = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/system/ping?time=${Date.now()}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
logClientError = async (message: string, level = 'ERROR') => {
|
||||
const url = `${this.getBaseRoute()}/logs`;
|
||||
|
||||
if (!this.enableLogging) {
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: 'Logging disabled.',
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
url,
|
||||
{method: 'post', body: JSON.stringify({message, level})},
|
||||
);
|
||||
};
|
||||
|
||||
getClientConfigOld = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/config/client?format=old`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getClientLicenseOld = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/license/client?format=old`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTimezones = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getTimezonesRoute()}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getDataRetentionPolicy = () => {
|
||||
return this.doFetch(
|
||||
`${this.getDataRetentionRoute()}/policy`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getRolesByNames = async (rolesNames: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getRolesRoute()}/names`,
|
||||
{method: 'post', body: JSON.stringify(rolesNames)},
|
||||
);
|
||||
};
|
||||
|
||||
getRedirectLocation = async (urlParam: string) => {
|
||||
if (!urlParam.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = `${this.getRedirectLocationRoute()}${buildQueryString({url: urlParam})}`;
|
||||
return this.doFetch(url, {method: 'get'});
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientGeneral;
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Group} from '@mm-redux/types/groups';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientGroupsMix {
|
||||
getGroups: (filterAllowReference?: boolean, page?: number, perPage?: number) => Promise<Group[]>;
|
||||
getGroupsByUserId: (userID: string) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannelsInTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
}
|
||||
|
||||
const ClientGroups = (superclass: any) => class extends superclass {
|
||||
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT, since = 0) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getGroupsByUserId = async (userID: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/${userID}/groups`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToChannelsInTeam = async (teamID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToChannel = async (channelID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientGroups;
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import mix from '@utils/mix';
|
||||
|
||||
import {DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID} from './constants';
|
||||
import ClientApps, {ClientAppsMix} from './apps';
|
||||
import ClientBase from './base';
|
||||
import ClientBots, {ClientBotsMix} from './bots';
|
||||
import ClientChannels, {ClientChannelsMix} from './channels';
|
||||
import ClientEmojis, {ClientEmojisMix} from './emojis';
|
||||
import ClientFiles, {ClientFilesMix} from './files';
|
||||
import ClientGeneral, {ClientGeneralMix} from './general';
|
||||
import ClientGroups, {ClientGroupsMix} from './groups';
|
||||
import ClientIntegrations, {ClientIntegrationsMix} from './integrations';
|
||||
import ClientPosts, {ClientPostsMix} from './posts';
|
||||
import ClientPreferences, {ClientPreferencesMix} from './preferences';
|
||||
import ClientSharedChannels, {ClientSharedChannelsMix} from './shared_channels';
|
||||
import ClientTeams, {ClientTeamsMix} from './teams';
|
||||
import ClientTos, {ClientTosMix} from './tos';
|
||||
import ClientUsers, {ClientUsersMix} from './users';
|
||||
|
||||
interface Client extends ClientBase,
|
||||
ClientAppsMix,
|
||||
ClientBotsMix,
|
||||
ClientChannelsMix,
|
||||
ClientEmojisMix,
|
||||
ClientFilesMix,
|
||||
ClientGeneralMix,
|
||||
ClientGroupsMix,
|
||||
ClientIntegrationsMix,
|
||||
ClientPostsMix,
|
||||
ClientPreferencesMix,
|
||||
ClientSharedChannelsMix,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
ClientApps,
|
||||
ClientBots,
|
||||
ClientChannels,
|
||||
ClientEmojis,
|
||||
ClientFiles,
|
||||
ClientGeneral,
|
||||
ClientGroups,
|
||||
ClientIntegrations,
|
||||
ClientPosts,
|
||||
ClientPreferences,
|
||||
ClientSharedChannels,
|
||||
ClientTeams,
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
||||
export {Client4, Client, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID};
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Command, DialogSubmission} from '@mm-redux/types/integrations';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientIntegrationsMix {
|
||||
getCommandsList: (teamId: string) => Promise<Command[]>;
|
||||
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, commandArgs?: Record<string, any>) => Promise<Command[]>;
|
||||
getAutocompleteCommandsList: (teamId: string, page?: number, perPage?: number) => Promise<Command[]>;
|
||||
executeCommand: (command: Command, commandArgs?: Record<string, any>) => Promise<any>;
|
||||
addCommand: (command: Command) => Promise<Command>;
|
||||
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
getCommandsList = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}?team_id=${teamId}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCommandAutocompleteSuggestionsList = async (userInput: string, teamId: string, commandArgs: {}) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({...commandArgs, user_input: userInput})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAutocompleteCommandsList = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
executeCommand = async (command: Command, commandArgs = {}) => {
|
||||
analytics.trackAPI('api_integrations_used');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}/execute`,
|
||||
{method: 'post', body: JSON.stringify({command, ...commandArgs})},
|
||||
);
|
||||
};
|
||||
|
||||
addCommand = async (command: Command) => {
|
||||
analytics.trackAPI('api_integrations_created');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(command)},
|
||||
);
|
||||
};
|
||||
|
||||
submitInteractiveDialog = async (data: DialogSubmission) => {
|
||||
analytics.trackAPI('api_interactive_messages_dialog_submitted');
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/actions/dialogs/submit`,
|
||||
{method: 'post', body: JSON.stringify(data)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientIntegrations;
|
||||
@@ -1,237 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {FileInfo} from '@mm-redux/types/files';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientPostsMix {
|
||||
createPost: (post: Post) => Promise<Post>;
|
||||
updatePost: (post: Post) => Promise<Post>;
|
||||
getPost: (postId: string) => Promise<Post>;
|
||||
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
|
||||
deletePost: (postId: string) => Promise<any>;
|
||||
getPostThread: (postId: string) => Promise<any>;
|
||||
getPosts: (channelId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPostsSince: (channelId: string, since: number) => Promise<any>;
|
||||
getPostsBefore: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
|
||||
getFlaggedPosts: (userId: string, channelId?: string, teamId?: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPinnedPosts: (channelId: string) => Promise<any>;
|
||||
markPostAsUnread: (userId: string, postId: string) => Promise<any>;
|
||||
pinPost: (postId: string) => Promise<any>;
|
||||
unpinPost: (postId: string) => Promise<any>;
|
||||
addReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
|
||||
removeReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
|
||||
getReactionsForPost: (postId: string) => Promise<any>;
|
||||
searchPostsWithParams: (teamId: string, params: any) => Promise<any>;
|
||||
searchPosts: (teamId: string, terms: string, isOrSearch: boolean) => Promise<any>;
|
||||
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientPosts = (superclass: any) => class extends superclass {
|
||||
createPost = async (post: Post) => {
|
||||
analytics.trackAPI('api_posts_create', {channel_id: post.channel_id});
|
||||
|
||||
if (post.root_id != null && post.root_id !== '') {
|
||||
analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id});
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(post)},
|
||||
);
|
||||
};
|
||||
|
||||
updatePost = async (post: Post) => {
|
||||
analytics.trackAPI('api_posts_update', {channel_id: post.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(post.id)}`,
|
||||
{method: 'put', body: JSON.stringify(post)},
|
||||
);
|
||||
};
|
||||
|
||||
getPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
patchPost = async (postPatch: Partial<Post> & {id: string}) => {
|
||||
analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postPatch.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(postPatch)},
|
||||
);
|
||||
};
|
||||
|
||||
deletePost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostThread = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/thread`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsSince = async (channelId: string, since: number) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({since})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_before', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_after', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getFileInfosForPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/files/info`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getFlaggedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_flagged', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPinnedPosts = async (channelId: string) => {
|
||||
analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/pinned`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
markPostAsUnread = async (userId: string, postId: string) => {
|
||||
analytics.trackAPI('api_post_set_unread_post');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/set_unread`,
|
||||
{method: 'post'},
|
||||
);
|
||||
}
|
||||
|
||||
pinPost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_pin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/pin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
unpinPost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_unpin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/unpin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
addReaction = async (userId: string, postId: string, emojiName: string) => {
|
||||
analytics.trackAPI('api_reactions_save', {post_id: postId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getReactionsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify({user_id: userId, post_id: postId, emoji_name: emojiName})},
|
||||
);
|
||||
};
|
||||
|
||||
removeReaction = async (userId: string, postId: string, emojiName: string) => {
|
||||
analytics.trackAPI('api_reactions_delete', {post_id: postId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getReactionsForPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/reactions`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
searchPostsWithParams = async (teamId: string, params: any) => {
|
||||
analytics.trackAPI('api_posts_search', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/posts/search`,
|
||||
{method: 'post', body: JSON.stringify(params)},
|
||||
);
|
||||
};
|
||||
|
||||
searchPosts = async (teamId: string, terms: string, isOrSearch: boolean) => {
|
||||
return this.searchPostsWithParams(teamId, {terms, is_or_search: isOrSearch});
|
||||
};
|
||||
|
||||
doPostAction = async (postId: string, actionId: string, selectedOption = '') => {
|
||||
return this.doPostActionWithCookie(postId, actionId, '', selectedOption);
|
||||
};
|
||||
|
||||
doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
|
||||
if (selectedOption) {
|
||||
analytics.trackAPI('api_interactive_messages_menu_selected');
|
||||
} else {
|
||||
analytics.trackAPI('api_interactive_messages_button_clicked');
|
||||
}
|
||||
|
||||
const msg: any = {
|
||||
selected_option: selectedOption,
|
||||
};
|
||||
if (actionCookie !== '') {
|
||||
msg.cookie = actionCookie;
|
||||
}
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/actions/${encodeURIComponent(actionId)}`,
|
||||
{method: 'post', body: JSON.stringify(msg)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPosts;
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {PreferenceType} from '@mm-redux/types/preferences';
|
||||
|
||||
export interface ClientPreferencesMix {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
|
||||
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
|
||||
getMyPreferences: () => Promise<PreferenceType>;
|
||||
}
|
||||
|
||||
const ClientPreferences = (superclass: any) => class extends superclass {
|
||||
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute(userId)}`,
|
||||
{method: 'put', body: JSON.stringify(preferences)},
|
||||
);
|
||||
};
|
||||
|
||||
getMyPreferences = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute('me')}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute(userId)}/delete`,
|
||||
{method: 'post', body: JSON.stringify(preferences)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPreferences;
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SharedChannel} from '@mm-redux/types/channels';
|
||||
import {RemoteCluster} from '@mm-redux/types/remote_cluster';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientSharedChannelsMix {
|
||||
getRemoteClusterInfo: (remote_id: string) => Promise<RemoteCluster>;
|
||||
getSharedChannels: (teamId: string, page: number, perPage?: number) => Promise<SharedChannel[]>;
|
||||
}
|
||||
|
||||
const ClientSharedChannels = (superclass: any) => class extends superclass {
|
||||
getRemoteClusterInfo = async (remote_id: string) => {
|
||||
const response = await this.doFetch(
|
||||
`${this.getSharedChannelsRoute()}/remote_info/${remote_id}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
return {
|
||||
...response,
|
||||
remote_id,
|
||||
};
|
||||
};
|
||||
|
||||
getSharedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getSharedChannelsRoute()}/${teamId}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientSharedChannels;
|
||||
@@ -1,177 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Team, TeamMembership, TeamUnread} from '@mm-redux/types/teams';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientTeamsMix {
|
||||
createTeam: (team: Team) => Promise<Team>;
|
||||
deleteTeam: (teamId: string) => Promise<any>;
|
||||
updateTeam: (team: Team) => Promise<Team>;
|
||||
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<any>;
|
||||
getTeam: (teamId: string) => Promise<Team>;
|
||||
getTeamByName: (teamName: string) => Promise<Team>;
|
||||
getMyTeams: () => Promise<Team[]>;
|
||||
getTeamsForUser: (userId: string) => Promise<Team[]>;
|
||||
getMyTeamMembers: () => Promise<TeamMembership[]>;
|
||||
getMyTeamUnreads: () => Promise<TeamUnread[]>;
|
||||
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
|
||||
getTeamMember: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
joinTeam: (inviteId: string) => Promise<TeamMembership>;
|
||||
removeFromTeam: (teamId: string, userId: string) => Promise<any>;
|
||||
getTeamStats: (teamId: string) => Promise<any>;
|
||||
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
|
||||
}
|
||||
|
||||
const ClientTeams = (superclass: any) => class extends superclass {
|
||||
createTeam = async (team: Team) => {
|
||||
analytics.trackAPI('api_teams_create');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteTeam = async (teamId: string) => {
|
||||
analytics.trackAPI('api_teams_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
updateTeam = async (team: Team) => {
|
||||
analytics.trackAPI('api_teams_update_name', {team_id: team.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(team.id)}`,
|
||||
{method: 'put', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
patchTeam = async (team: Partial<Team> & {id: string}) => {
|
||||
analytics.trackAPI('api_teams_patch_name', {team_id: team.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(team.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeam = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
this.getTeamRoute(teamId),
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamByName = async (teamName: string) => {
|
||||
analytics.trackAPI('api_teams_get_team_by_name');
|
||||
|
||||
return this.doFetch(
|
||||
this.getTeamNameRoute(teamName),
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeams = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamsForUser = async (userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/teams`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeamMembers = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/members`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeamUnreads = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/unread`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamMembers = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamMember = async (teamId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMemberRoute(teamId, userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
addToTeam = async (teamId: string, userId: string) => {
|
||||
analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
|
||||
|
||||
const member = {user_id: userId, team_id: teamId};
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}`,
|
||||
{method: 'post', body: JSON.stringify(member)},
|
||||
);
|
||||
};
|
||||
|
||||
joinTeam = async (inviteId: string) => {
|
||||
const query = buildQueryString({invite_id: inviteId});
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}/members/invite${query}`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
removeFromTeam = async (teamId: string, userId: string) => {
|
||||
analytics.trackAPI('api_teams_remove_members', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamMemberRoute(teamId, userId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamStats = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/stats`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamIconUrl = (teamId: string, lastTeamIconUpdate: number) => {
|
||||
const params: any = {};
|
||||
if (lastTeamIconUpdate) {
|
||||
params._ = lastTeamIconUpdate;
|
||||
}
|
||||
|
||||
return `${this.getTeamRoute(teamId)}/image${buildQueryString(params)}`;
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientTeams;
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientTosMix {
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<any>;
|
||||
getTermsOfService: () => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientTos = (superclass: any) => class extends superclass {
|
||||
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/terms_of_service`,
|
||||
{method: 'post', body: JSON.stringify({termsOfServiceId, accepted})},
|
||||
);
|
||||
}
|
||||
|
||||
getTermsOfService = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/terms_of_service`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientTos;
|
||||
@@ -1,422 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {UserCustomStatus, UserProfile, UserStatus} from '@mm-redux/types/users';
|
||||
import {buildQueryString, isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientUsersMix {
|
||||
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
|
||||
patchMe: (userPatch: Partial<UserProfile>) => Promise<UserProfile>;
|
||||
patchUser: (userPatch: Partial<UserProfile> & {id: string}) => Promise<UserProfile>;
|
||||
updateUser: (user: UserProfile) => Promise<UserProfile>;
|
||||
demoteUserToGuest: (userId: string) => Promise<any>;
|
||||
getKnownUsers: () => Promise<string[]>;
|
||||
sendPasswordResetEmail: (email: string) => Promise<any>;
|
||||
setDefaultProfileImage: (userId: string) => Promise<any>;
|
||||
login: (loginId: string, password: string, token?: string, deviceId?: string, ldapOnly?: boolean) => Promise<UserProfile>;
|
||||
loginById: (id: string, password: string, token?: string, deviceId?: string) => Promise<UserProfile>;
|
||||
logout: () => Promise<any>;
|
||||
getProfiles: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesByIds: (userIds: string[], options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesByUsernames: (usernames: string[]) => Promise<UserProfile[]>;
|
||||
getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
|
||||
getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesInChannel: (channelId: string, page?: number, perPage?: number, sort?: string) => Promise<UserProfile[]>;
|
||||
getProfilesInGroupChannels: (channelsIds: string[]) => Promise<{[x: string]: UserProfile[]}>;
|
||||
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
|
||||
getMe: () => Promise<UserProfile>;
|
||||
getUser: (userId: string) => Promise<UserProfile>;
|
||||
getUserByUsername: (username: string) => Promise<UserProfile>;
|
||||
getUserByEmail: (email: string) => Promise<UserProfile>;
|
||||
getProfilePictureUrl: (userId: string, lastPictureUpdate: number) => string;
|
||||
getDefaultProfilePictureUrl: (userId: string) => string;
|
||||
autocompleteUsers: (name: string, teamId: string, channelId: string, options?: Record<string, any>) => Promise<{users: UserProfile[], out_of_channel?: UserProfile[]}>;
|
||||
getSessions: (userId: string) => Promise<any>;
|
||||
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
|
||||
attachDevice: (deviceId: string) => Promise<any>;
|
||||
searchUsers: (term: string, options: any) => Promise<UserProfile[]>;
|
||||
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
|
||||
getStatus: (userId: string) => Promise<UserStatus>;
|
||||
updateStatus: (status: UserStatus) => Promise<UserStatus>;
|
||||
updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
unsetCustomStatus: () => Promise<{status: string}>;
|
||||
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
}
|
||||
|
||||
const ClientUsers = (superclass: any) => class extends superclass {
|
||||
createUser = async (user: UserProfile, token: string, inviteId: string) => {
|
||||
analytics.trackAPI('api_users_create');
|
||||
|
||||
const queryParams: any = {};
|
||||
|
||||
if (token) {
|
||||
queryParams.t = token;
|
||||
}
|
||||
|
||||
if (inviteId) {
|
||||
queryParams.iid = inviteId;
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryParams)}`,
|
||||
{method: 'post', body: JSON.stringify(user)},
|
||||
);
|
||||
}
|
||||
|
||||
patchMe = async (userPatch: Partial<UserProfile>) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/patch`,
|
||||
{method: 'put', body: JSON.stringify(userPatch)},
|
||||
);
|
||||
}
|
||||
|
||||
patchUser = async (userPatch: Partial<UserProfile> & {id: string}) => {
|
||||
analytics.trackAPI('api_users_patch');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userPatch.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(userPatch)},
|
||||
);
|
||||
}
|
||||
|
||||
updateUser = async (user: UserProfile) => {
|
||||
analytics.trackAPI('api_users_update');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(user.id)}`,
|
||||
{method: 'put', body: JSON.stringify(user)},
|
||||
);
|
||||
}
|
||||
|
||||
demoteUserToGuest = async (userId: string) => {
|
||||
analytics.trackAPI('api_users_demote_user_to_guest');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/demote`,
|
||||
{method: 'post'},
|
||||
);
|
||||
}
|
||||
|
||||
getKnownUsers = async () => {
|
||||
analytics.trackAPI('api_get_known_users');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/known`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
sendPasswordResetEmail = async (email: string) => {
|
||||
analytics.trackAPI('api_users_send_password_reset');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/password/reset/send`,
|
||||
{method: 'post', body: JSON.stringify({email})},
|
||||
);
|
||||
}
|
||||
|
||||
setDefaultProfileImage = async (userId: string) => {
|
||||
analytics.trackAPI('api_users_set_default_profile_picture');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/image`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => {
|
||||
analytics.trackAPI('api_users_login');
|
||||
|
||||
if (ldapOnly) {
|
||||
analytics.trackAPI('api_users_login_ldap');
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
device_id: deviceId,
|
||||
login_id: loginId,
|
||||
password,
|
||||
token,
|
||||
};
|
||||
|
||||
if (ldapOnly) {
|
||||
body.ldap_only = 'true';
|
||||
}
|
||||
|
||||
const {data} = await this.doFetchWithResponse(
|
||||
`${this.getUsersRoute()}/login`,
|
||||
{
|
||||
method: 'post',
|
||||
body: JSON.stringify(body),
|
||||
headers: {'Cache-Control': 'no-store'},
|
||||
},
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
loginById = async (id: string, password: string, token = '', deviceId = '') => {
|
||||
analytics.trackAPI('api_users_login');
|
||||
const body: any = {
|
||||
device_id: deviceId,
|
||||
id,
|
||||
password,
|
||||
token,
|
||||
};
|
||||
|
||||
const {data} = await this.doFetchWithResponse(
|
||||
`${this.getUsersRoute()}/login`,
|
||||
{method: 'post', body: JSON.stringify(body)},
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
logout = async () => {
|
||||
analytics.trackAPI('api_users_logout');
|
||||
|
||||
const {response} = await this.doFetchWithResponse(
|
||||
`${this.getUsersRoute()}/logout`,
|
||||
{method: 'post'},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
this.token = '';
|
||||
}
|
||||
|
||||
this.serverVersion = '';
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
|
||||
analytics.trackAPI('api_profiles_get');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesByIds = async (userIds: string[], options = {}) => {
|
||||
analytics.trackAPI('api_profiles_get_by_ids');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesByUsernames = async (usernames: string[]) => {
|
||||
analytics.trackAPI('api_profiles_get_by_usernames');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/usernames`,
|
||||
{method: 'post', body: JSON.stringify(usernames)},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => {
|
||||
analytics.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString({...options, in_team: teamId, page, per_page: perPage, sort})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesNotInTeam = async (teamId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_profiles_get_not_in_team', {team_id: teamId, group_constrained: groupConstrained});
|
||||
|
||||
const queryStringObj: any = {not_in_team: teamId, page, per_page: perPage};
|
||||
if (groupConstrained) {
|
||||
queryStringObj.group_constrained = true;
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
|
||||
analytics.trackAPI('api_profiles_get_without_team');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
|
||||
analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
|
||||
|
||||
const serverVersion = this.getServerVersion();
|
||||
let queryStringObj;
|
||||
if (isMinimumServerVersion(serverVersion, 4, 7)) {
|
||||
queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
|
||||
} else {
|
||||
queryStringObj = {in_channel: channelId, page, per_page: perPage};
|
||||
}
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesInGroupChannels = async (channelsIds: string[]) => {
|
||||
analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/group_channels`,
|
||||
{method: 'post', body: JSON.stringify(channelsIds)},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesNotInChannel = async (teamId: string, channelId: string, groupConstrained: boolean, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_profiles_get_not_in_channel', {team_id: teamId, channel_id: channelId, group_constrained: groupConstrained});
|
||||
|
||||
const queryStringObj: any = {in_team: teamId, not_in_channel: channelId, page, per_page: perPage};
|
||||
if (groupConstrained) {
|
||||
queryStringObj.group_constrained = true;
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMe = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getUser = async (userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getUserByUsername = async (username: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/username/${username}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getUserByEmail = async (email: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/email/${email}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilePictureUrl = (userId: string, lastPictureUpdate: number) => {
|
||||
const params: any = {};
|
||||
|
||||
if (lastPictureUpdate) {
|
||||
params._ = lastPictureUpdate;
|
||||
}
|
||||
|
||||
return `${this.getUserRoute(userId)}/image${buildQueryString(params)}`;
|
||||
};
|
||||
|
||||
getDefaultProfilePictureUrl = (userId: string) => {
|
||||
return `${this.getUserRoute(userId)}/image/default`;
|
||||
};
|
||||
|
||||
autocompleteUsers = async (name: string, teamId: string, channelId: string, options = {
|
||||
limit: General.AUTOCOMPLETE_LIMIT_DEFAULT,
|
||||
}) => {
|
||||
return this.doFetch(`${this.getUsersRoute()}/autocomplete${buildQueryString({
|
||||
in_team: teamId,
|
||||
in_channel: channelId,
|
||||
name,
|
||||
limit: options.limit,
|
||||
})}`, {
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
getSessions = async (userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/sessions`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
checkUserMfa = async (loginId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/mfa`,
|
||||
{method: 'post', body: JSON.stringify({login_id: loginId})},
|
||||
);
|
||||
};
|
||||
|
||||
attachDevice = async (deviceId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/sessions/device`,
|
||||
{method: 'put', body: JSON.stringify({device_id: deviceId})},
|
||||
);
|
||||
};
|
||||
|
||||
searchUsers = async (term: string, options: any) => {
|
||||
analytics.trackAPI('api_search_users');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/search`,
|
||||
{method: 'post', body: JSON.stringify({term, ...options})},
|
||||
);
|
||||
};
|
||||
|
||||
getStatusesByIds = async (userIds: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/status/ids`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
getStatus = async (userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/status`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
updateStatus = async (status: UserStatus) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(status.user_id)}/status`,
|
||||
{method: 'put', body: JSON.stringify(status)},
|
||||
);
|
||||
};
|
||||
|
||||
updateCustomStatus = (customStatus: UserCustomStatus) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom`,
|
||||
{method: 'put', body: JSON.stringify(customStatus)},
|
||||
);
|
||||
};
|
||||
|
||||
unsetCustomStatus = () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
removeRecentCustomStatus = (customStatus: UserCustomStatus) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/status/custom/recent/delete`,
|
||||
{method: 'post', body: JSON.stringify(customStatus)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientUsers;
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import Store from '@store/store';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
|
||||
@@ -15,34 +11,22 @@ const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
class WebSocketClient {
|
||||
conn?: WebSocket;
|
||||
connectionUrl: string;
|
||||
connectionTimeout: any;
|
||||
connectionId: string | null;
|
||||
token: string|null;
|
||||
|
||||
// responseSequence is the number to track a response sent
|
||||
// via the websocket. A response will always have the same sequence number
|
||||
// as the request.
|
||||
responseSequence: number;
|
||||
|
||||
// serverSequence is the incrementing sequence number from the
|
||||
// server-sent event stream.
|
||||
serverSequence: number;
|
||||
sequence: number;
|
||||
connectFailCount: number;
|
||||
eventCallback?: Function;
|
||||
firstConnectCallback?: Function;
|
||||
missedEventsCallback?: Function;
|
||||
reconnectCallback?: Function;
|
||||
errorCallback?: Function;
|
||||
closeCallback?: Function;
|
||||
connectingCallback?: Function;
|
||||
stop: boolean;
|
||||
connectionTimeout: any;
|
||||
|
||||
constructor() {
|
||||
this.connectionUrl = '';
|
||||
this.connectionId = '';
|
||||
this.token = null;
|
||||
this.responseSequence = 1;
|
||||
this.serverSequence = 0;
|
||||
this.sequence = 1;
|
||||
this.connectFailCount = 0;
|
||||
this.stop = false;
|
||||
}
|
||||
@@ -71,6 +55,10 @@ class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
}
|
||||
@@ -99,30 +87,11 @@ class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = connectionUrl;
|
||||
const config = getConfig(Store.redux?.getState());
|
||||
const reliableWebSockets = config?.EnableReliableWebSockets === 'true';
|
||||
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
url = `${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}`;
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + url); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
this.conn = new WebSocket(url, [], {headers: {origin}, ...(additionalOptions || {})});
|
||||
this.conn = new WebSocket(connectionUrl, [], {headers: {origin}, ...(additionalOptions || {})});
|
||||
this.connectionUrl = connectionUrl;
|
||||
this.token = token;
|
||||
|
||||
this.conn!.onopen = () => {
|
||||
// No need to reset sequence number here.
|
||||
if (!reliableWebSockets) {
|
||||
this.serverSequence = 0;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
@@ -131,10 +100,8 @@ class WebSocketClient {
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||
if (!reliableWebSockets && this.reconnectCallback) {
|
||||
if (this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
} else if (reliableWebSockets && this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
this.firstConnectCallback();
|
||||
@@ -146,7 +113,7 @@ class WebSocketClient {
|
||||
|
||||
this.conn!.onclose = () => {
|
||||
this.conn = undefined;
|
||||
this.responseSequence = 1;
|
||||
this.sequence = 1;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
@@ -197,55 +164,11 @@ class WebSocketClient {
|
||||
|
||||
this.conn!.onmessage = (evt: any) => {
|
||||
const msg = JSON.parse(evt.data);
|
||||
|
||||
// This indicates a reply to a websocket request.
|
||||
// We ignore sequence number validation of message responses
|
||||
// and only focus on the purely server side event stream.
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg); //eslint-disable-line no-console
|
||||
}
|
||||
} else if (this.eventCallback) {
|
||||
if (reliableWebSockets) {
|
||||
// We check the hello packet, which is always the first packet in a stream.
|
||||
if (msg.event === WebsocketEvents.HELLO && this.reconnectCallback) {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log('got connection id ', msg.data.connection_id);
|
||||
|
||||
// If we already have a connectionId present, and server sends a different one,
|
||||
// that means it's either a long timeout, or server restart, or sequence number is not found.
|
||||
// Then we do the sync calls, and reset sequence number to 0.
|
||||
if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) {
|
||||
//eslint-disable-next-line no-console
|
||||
console.log('long timeout, or server restart, or sequence number is not found.');
|
||||
this.reconnectCallback();
|
||||
this.serverSequence = 0;
|
||||
}
|
||||
|
||||
// If it's a fresh connection, we have to set the connectionId regardless.
|
||||
// And if it's an existing connection, setting it again is harmless, and keeps the code simple.
|
||||
this.connectionId = msg.data.connection_id;
|
||||
}
|
||||
|
||||
// Now we check for sequence number, and if it does not match,
|
||||
// we just disconnect and reconnect.
|
||||
if (msg.seq !== this.serverSequence) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
|
||||
// We are not calling this.close() because we need to auto-restart.
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
this.conn?.close(); // Will auto-reconnect after MIN_WEBSOCKET_RETRY_TIME.
|
||||
return;
|
||||
}
|
||||
} else if (msg.seq !== this.serverSequence && this.reconnectCallback) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('missed websocket event, act_seq=' + msg.seq + ' exp_seq=' + this.serverSequence);
|
||||
this.reconnectCallback();
|
||||
}
|
||||
|
||||
this.serverSequence = msg.seq + 1;
|
||||
this.eventCallback(msg);
|
||||
}
|
||||
};
|
||||
@@ -264,10 +187,6 @@ class WebSocketClient {
|
||||
this.firstConnectCallback = callback;
|
||||
}
|
||||
|
||||
setMissedEventsCallback(callback: Function) {
|
||||
this.missedEventsCallback = callback;
|
||||
}
|
||||
|
||||
setReconnectCallback(callback: Function) {
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
@@ -283,17 +202,19 @@ class WebSocketClient {
|
||||
close(stop = false) {
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
|
||||
this.sequence = 1;
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.onclose = () => {}; //eslint-disable-line @typescript-eslint/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.responseSequence++,
|
||||
seq: this.sequence++,
|
||||
data,
|
||||
};
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
exports[`AtMention should match snapshot, no highlight 1`] = `
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={Array []}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@John.Smith
|
||||
</Text>
|
||||
@@ -20,11 +22,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
@@ -33,8 +31,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
"color": "#ff0000",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffe577",
|
||||
"color": "#166de0",
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -48,11 +45,7 @@ exports[`AtMention should match snapshot, without highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
|
||||
@@ -10,20 +10,21 @@ import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
|
||||
import {showModal} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import BottomSheet from '@utils/bottom_sheet';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
|
||||
export default class AtMention extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isSearchResult: PropTypes.bool,
|
||||
mentionKeys: PropTypes.array,
|
||||
mentionKeys: PropTypes.array.isRequired,
|
||||
mentionName: PropTypes.string.isRequired,
|
||||
mentionStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
mentionStyle: CustomPropTypes.Style,
|
||||
onPostPress: PropTypes.func,
|
||||
textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
usersByUsername: PropTypes.object,
|
||||
usersByUsername: PropTypes.object.isRequired,
|
||||
groupsByName: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -64,7 +65,6 @@ export default class AtMention extends React.PureComponent {
|
||||
leftButtons: [{
|
||||
id: 'close-settings',
|
||||
icon: this.closeButton,
|
||||
testID: 'close.settings.button',
|
||||
}],
|
||||
},
|
||||
};
|
||||
@@ -139,7 +139,7 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys, theme} = this.props;
|
||||
const {isSearchResult, mentionName, mentionStyle, onPostPress, teammateNameDisplay, textStyle, mentionKeys} = this.props;
|
||||
const {user} = this.state;
|
||||
const mentionTextStyle = [];
|
||||
|
||||
@@ -155,8 +155,9 @@ export default class AtMention extends React.PureComponent {
|
||||
let styleText;
|
||||
|
||||
if (textStyle) {
|
||||
backgroundColor = theme.mentionHighlightBg;
|
||||
styleText = textStyle;
|
||||
const {backgroundColor: bg, ...otherStyles} = StyleSheet.flatten(textStyle);
|
||||
backgroundColor = bg;
|
||||
styleText = otherStyles;
|
||||
}
|
||||
|
||||
if (user?.username) {
|
||||
@@ -175,12 +176,12 @@ export default class AtMention extends React.PureComponent {
|
||||
} else {
|
||||
const pattern = new RegExp(/\b(all|channel|here)(?:\.\B|_\b|\b)/, 'i');
|
||||
const mentionMatch = pattern.exec(mentionName);
|
||||
highlighted = true;
|
||||
|
||||
if (mentionMatch) {
|
||||
mention = mentionMatch.length > 1 ? mentionMatch[1] : mentionMatch[0];
|
||||
suffix = mentionName.replace(mention, '');
|
||||
isMention = true;
|
||||
highlighted = true;
|
||||
} else {
|
||||
mention = mentionName;
|
||||
}
|
||||
@@ -193,7 +194,7 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (suffix) {
|
||||
const suffixStyle = {...StyleSheet.flatten(styleText), color: theme.centerChannelColor};
|
||||
const suffixStyle = {...styleText, color: this.props.theme.centerChannelColor};
|
||||
suffixElement = (
|
||||
<Text style={suffixStyle}>
|
||||
{suffix}
|
||||
@@ -206,7 +207,7 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (highlighted) {
|
||||
mentionTextStyle.push({backgroundColor, color: theme.mentionHighlightLink});
|
||||
mentionTextStyle.push({backgroundColor});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import AtMention from './at_mention.js';
|
||||
|
||||
describe('AtMention', () => {
|
||||
@@ -16,7 +13,7 @@ describe('AtMention', () => {
|
||||
mentionName: 'John.Smith',
|
||||
mentionStyle: {color: '#ff0000'},
|
||||
textStyle: {backgroundColor: 'yellow'},
|
||||
theme: Preferences.THEMES.default,
|
||||
theme: {},
|
||||
};
|
||||
|
||||
test('should match snapshot, no highlight', () => {
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getAllGroupsForReferenceByName} from '@mm-redux/selectors/entities/groups';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
|
||||
import {getUsersByUsername} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
import {getAllUserMentionKeys} from '@mm-redux/selectors/entities/search';
|
||||
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import {getAllGroupsForReferenceByName} from '@mm-redux/selectors/entities/groups';
|
||||
|
||||
import AtMention from './at_mention';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
|
||||
@@ -2,21 +2,25 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, StatusBar} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Permissions from 'react-native-permissions';
|
||||
import {Alert, StatusBar} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {VALID_MIME_TYPES} from '@screens/edit_profile/edit_profile';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
|
||||
|
||||
import AttachmentButton from './index';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn().mockImplementation((options, callback) => callback({didCancel: true})),
|
||||
launchImageLibrary: jest.fn().mockImplementation((options, callback) => callback({didCancel: true})),
|
||||
}));
|
||||
|
||||
describe('AttachmentButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
maxFileSize: 10,
|
||||
@@ -24,7 +28,7 @@ describe('AttachmentButton', () => {
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(<AttachmentButton {...baseProps}/>);
|
||||
const wrapper = shallow(<AttachmentButton {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -36,7 +40,7 @@ describe('AttachmentButton', () => {
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<AttachmentButton {...props}/>);
|
||||
const wrapper = shallow(<AttachmentButton {...props}/>);
|
||||
|
||||
const file = {
|
||||
type: 'image/gif',
|
||||
@@ -55,7 +59,7 @@ describe('AttachmentButton', () => {
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(<AttachmentButton {...props}/>);
|
||||
const wrapper = shallow(<AttachmentButton {...props}/>);
|
||||
|
||||
const file = {
|
||||
fileSize: 10,
|
||||
@@ -73,8 +77,9 @@ describe('AttachmentButton', () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPhotoPermission = await wrapper.instance().hasPhotoPermission('camera');
|
||||
@@ -88,8 +93,9 @@ describe('AttachmentButton', () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.BLOCKED);
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPhotoPermission = await wrapper.instance().hasPhotoPermission('camera');
|
||||
@@ -100,8 +106,9 @@ describe('AttachmentButton', () => {
|
||||
});
|
||||
|
||||
test('should re-enable StatusBar after ImagePicker launchCamera finishes', async () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
@@ -113,8 +120,9 @@ describe('AttachmentButton', () => {
|
||||
});
|
||||
|
||||
test('should re-enable StatusBar after ImagePicker launchImageLibrary finishes', async () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, SectionList} from 'react-native';
|
||||
import {SectionList} from 'react-native';
|
||||
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
|
||||
import AtMentionItem from '@components/autocomplete/at_mention_item';
|
||||
import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from '@components/autocomplete/special_mention_item';
|
||||
import GroupMentionItem from '@components/autocomplete/at_mention_group/at_mention_group';
|
||||
import {RequestStatus} from '@mm-redux/constants';
|
||||
import {debounce} from '@mm-redux/actions/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
|
||||
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
|
||||
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
|
||||
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
|
||||
import GroupMentionItem from 'app/components/autocomplete/at_mention_group/at_mention_group';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
export default class AtMention extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -54,15 +54,9 @@ export default class AtMention extends PureComponent {
|
||||
sections: [],
|
||||
};
|
||||
}
|
||||
|
||||
runSearch = debounce((currentTeamId, channelId, matchTerm) => {
|
||||
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
|
||||
}, 200);
|
||||
|
||||
updateSections(sections) {
|
||||
this.setState({sections});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.props.matchTerm !== prevProps.matchTerm) {
|
||||
if (this.props.matchTerm === null) {
|
||||
@@ -74,9 +68,9 @@ export default class AtMention extends PureComponent {
|
||||
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
|
||||
|
||||
// Update user autocomplete list with results of server request
|
||||
const {currentTeamId, currentChannelId, matchTerm} = this.props;
|
||||
const {currentTeamId, currentChannelId} = this.props;
|
||||
const channelId = this.props.isSearch ? '' : currentChannelId;
|
||||
this.runSearch(currentTeamId, channelId, matchTerm);
|
||||
this.props.actions.autocompleteUsers(this.props.matchTerm, currentTeamId, channelId);
|
||||
}
|
||||
}
|
||||
if (this.props.matchTerm !== null && this.props.matchTerm === prevProps.matchTerm) {
|
||||
@@ -257,16 +251,15 @@ export default class AtMention extends PureComponent {
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
testID='at_mention_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
testID='at_mention_suggestion.list'
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,10 @@ import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/preferences';
|
||||
@@ -20,7 +18,6 @@ interface AtMentionItemProps {
|
||||
isBot: boolean;
|
||||
isCurrentUser: boolean;
|
||||
isGuest: boolean;
|
||||
isShared: boolean;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
onPress: (username: string) => void;
|
||||
@@ -48,10 +45,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
rowInfo: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
rowFullname: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
@@ -73,7 +66,6 @@ const AtMentionItem = (props: AtMentionItemProps) => {
|
||||
isBot,
|
||||
isCurrentUser,
|
||||
isGuest,
|
||||
isShared,
|
||||
lastName,
|
||||
nickname,
|
||||
onPress,
|
||||
@@ -123,52 +115,35 @@ const AtMentionItem = (props: AtMentionItemProps) => {
|
||||
size={24}
|
||||
status={null}
|
||||
showStatus={false}
|
||||
testID='at_mention_item.profile_picture'
|
||||
/>
|
||||
</View>
|
||||
<View style={style.rowInfo}>
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(name.length) &&
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
testID='at_mention_item.name'
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
testID='at_mention_item.username'
|
||||
>
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage='(you)'
|
||||
/>}
|
||||
{` @${username}`}
|
||||
</Text>
|
||||
</View>
|
||||
{isShared && (
|
||||
<ChannelIcon
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={true}
|
||||
isUnread={true}
|
||||
size={18}
|
||||
shared={true}
|
||||
theme={theme}
|
||||
type={General.DM_CHANNEL}
|
||||
/>
|
||||
)}
|
||||
<BotTag
|
||||
show={isBot}
|
||||
theme={theme}
|
||||
/>
|
||||
<GuestTag
|
||||
show={isGuest}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(name.length) &&
|
||||
<Text
|
||||
style={style.rowFullname}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
style={style.rowUsername}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{isCurrentUser &&
|
||||
<FormattedText
|
||||
id='suggestion.mention.you'
|
||||
defaultMessage='(you)'
|
||||
/>}
|
||||
{` @${username}`}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {connect} from 'react-redux';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {isShared} from '@mm-redux/utils/user_utils';
|
||||
import {isGuest} from '@utils/users';
|
||||
|
||||
import AtMentionItem from './at_mention_item';
|
||||
@@ -22,7 +21,6 @@ function mapStateToProps(state, ownProps) {
|
||||
showFullName: config.ShowFullName,
|
||||
isBot: Boolean(user.is_bot),
|
||||
isGuest: isGuest(user),
|
||||
isShared: isShared(user),
|
||||
theme: getTheme(state),
|
||||
isCurrentUser: getCurrentUserId(state) === user.id,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {t} from 'app/utils/i18n';
|
||||
export default class ChannelMention extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
searchChannels: PropTypes.func.isRequired,
|
||||
autocompleteChannelsForSearch: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
@@ -205,7 +206,6 @@ export default class ChannelMention extends PureComponent {
|
||||
<ChannelMentionItem
|
||||
channelId={item}
|
||||
onPress={this.completeMention}
|
||||
testID={`autocomplete.channel_mention.item.${item}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -224,16 +224,15 @@ export default class ChannelMention extends PureComponent {
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
testID='channel_mention_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
testID='channel_mention_suggestion.list'
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -56,6 +56,7 @@ function mapStateToProps(state, ownProps) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
searchChannels,
|
||||
autocompleteChannelsForSearch,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -47,8 +47,6 @@ const ChannelMentionItem = (props) => {
|
||||
isGuest,
|
||||
name,
|
||||
onPress,
|
||||
shared,
|
||||
testID,
|
||||
theme,
|
||||
type,
|
||||
} = props;
|
||||
@@ -65,9 +63,7 @@ const ChannelMentionItem = (props) => {
|
||||
const margins = {marginLeft: insets.left, marginRight: insets.right};
|
||||
let iconName = 'globe';
|
||||
let component;
|
||||
if (shared) {
|
||||
iconName = type === General.PRIVATE_CHANNEL ? 'circle-multiple-outline-lock' : 'circle-multiple-outline';
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
if (type === General.PRIVATE_CHANNEL) {
|
||||
iconName = 'lock';
|
||||
}
|
||||
|
||||
@@ -81,7 +77,6 @@ const ChannelMentionItem = (props) => {
|
||||
key={channelId}
|
||||
onPress={completeMention}
|
||||
style={[style.row, margins]}
|
||||
testID={testID}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
|
||||
@@ -102,7 +97,6 @@ const ChannelMentionItem = (props) => {
|
||||
onPress={completeMention}
|
||||
style={margins}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
testID={testID}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
|
||||
@@ -30,7 +30,6 @@ function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
displayName,
|
||||
name: channel?.name,
|
||||
shared: channel?.shared,
|
||||
type: channel?.type,
|
||||
isBot,
|
||||
isGuest,
|
||||
|
||||
@@ -200,7 +200,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"bust_in_silhouette",
|
||||
"busts_in_silhouette",
|
||||
"butterfly",
|
||||
"ca",
|
||||
"cactus",
|
||||
"cake",
|
||||
"calendar",
|
||||
@@ -1024,7 +1023,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"pisces",
|
||||
"pitcairn_islands",
|
||||
"pizza",
|
||||
"pk",
|
||||
"place_of_worship",
|
||||
"plate_with_cutlery",
|
||||
"play_or_pause_button",
|
||||
@@ -1507,7 +1505,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"yen",
|
||||
"yin_yang",
|
||||
"yum",
|
||||
"za",
|
||||
"zambia",
|
||||
"zap",
|
||||
"zero",
|
||||
@@ -1715,7 +1712,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"bust_in_silhouette",
|
||||
"busts_in_silhouette",
|
||||
"butterfly",
|
||||
"ca",
|
||||
"cactus",
|
||||
"cake",
|
||||
"calendar",
|
||||
@@ -2539,7 +2535,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"pisces",
|
||||
"pitcairn_islands",
|
||||
"pizza",
|
||||
"pk",
|
||||
"place_of_worship",
|
||||
"plate_with_cutlery",
|
||||
"play_or_pause_button",
|
||||
@@ -3022,7 +3017,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"yen",
|
||||
"yin_yang",
|
||||
"yum",
|
||||
"za",
|
||||
"zambia",
|
||||
"zap",
|
||||
"zero",
|
||||
@@ -3042,7 +3036,7 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
numColumns={1}
|
||||
onEndReachedThreshold={2}
|
||||
pageSize={10}
|
||||
removeClippedSubviews={true}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
style={
|
||||
|
||||
@@ -230,7 +230,6 @@ export default class EmojiSuggestion extends PureComponent {
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
|
||||
<FlatList
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"Complete": "thetrigger",
|
||||
"Description": "The Description",
|
||||
"Hint": "The Hint",
|
||||
"IconData": "iconurl.com",
|
||||
"Suggestion": "/thetrigger",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
extraData={
|
||||
Object {
|
||||
"active": true,
|
||||
"dataSource": Array [
|
||||
Object {
|
||||
"Complete": "thetrigger",
|
||||
"Description": "The Description",
|
||||
"Hint": "The Hint",
|
||||
"IconData": "iconurl.com",
|
||||
"Suggestion": "/thetrigger",
|
||||
},
|
||||
],
|
||||
"lastCommandRequest": 1234,
|
||||
}
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={10}
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
"flex": 1,
|
||||
"paddingTop": 8,
|
||||
},
|
||||
Object {
|
||||
"maxHeight": 50,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="slash_suggestion.list"
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
/>
|
||||
`;
|
||||
@@ -1,953 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {
|
||||
thunk,
|
||||
configureStore,
|
||||
Client4,
|
||||
AppBinding,
|
||||
checkForExecuteSuggestion,
|
||||
} from './tests/app_command_parser_test_dependencies';
|
||||
|
||||
import {
|
||||
AppCallResponseTypes,
|
||||
AppCallTypes,
|
||||
AutocompleteSuggestion,
|
||||
} from './app_command_parser_dependencies';
|
||||
|
||||
import {
|
||||
AppCommandParser,
|
||||
ParseState,
|
||||
ParsedCommand,
|
||||
} from './app_command_parser';
|
||||
|
||||
import {
|
||||
reduxTestState,
|
||||
testBindings,
|
||||
} from './tests/app_command_parser_test_data';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('AppCommandParser', () => {
|
||||
const makeStore = async (bindings: AppBinding[]) => {
|
||||
const initialState = {
|
||||
...reduxTestState,
|
||||
entities: {
|
||||
...reduxTestState.entities,
|
||||
apps: {bindings},
|
||||
},
|
||||
} as any;
|
||||
const testStore = await mockStore(initialState);
|
||||
|
||||
return testStore;
|
||||
};
|
||||
|
||||
const intl = {
|
||||
formatMessage: (message: {id: string, defaultMessage: string}) => {
|
||||
return message.defaultMessage;
|
||||
},
|
||||
};
|
||||
|
||||
let parser: AppCommandParser;
|
||||
beforeEach(async () => {
|
||||
const store = await makeStore(testBindings);
|
||||
parser = new AppCommandParser(store as any, intl, 'current_channel_id', 'root_id');
|
||||
});
|
||||
|
||||
type Variant = {
|
||||
expectError?: string;
|
||||
verify?(parsed: ParsedCommand): void;
|
||||
}
|
||||
|
||||
type TC = {
|
||||
title: string;
|
||||
command: string;
|
||||
submit: Variant;
|
||||
autocomplete?: Variant; // if undefined, use same checks as submnit
|
||||
}
|
||||
|
||||
const checkResult = (parsed: ParsedCommand, v: Variant) => {
|
||||
if (v.expectError) {
|
||||
expect(parsed.state).toBe(ParseState.Error);
|
||||
expect(parsed.error).toBe(v.expectError);
|
||||
} else {
|
||||
// expect(parsed).toBe(1);
|
||||
expect(parsed.error).toBe('');
|
||||
expect(v.verify).toBeTruthy();
|
||||
if (v.verify) {
|
||||
v.verify(parsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getSuggestionsBase', () => {
|
||||
test('string matches 1', () => {
|
||||
const res = parser.getSuggestionsBase('/');
|
||||
expect(res).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('string matches 2', () => {
|
||||
const res = parser.getSuggestionsBase('/ji');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string matches 3', () => {
|
||||
const res = parser.getSuggestionsBase('/jira');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string matches case insensitive', () => {
|
||||
const res = parser.getSuggestionsBase('/JiRa');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string is past base command', () => {
|
||||
const res = parser.getSuggestionsBase('/jira ');
|
||||
expect(res).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('other command matches', () => {
|
||||
const res = parser.getSuggestionsBase('/other');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string does not match', () => {
|
||||
const res = parser.getSuggestionsBase('/wrong');
|
||||
expect(res).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchBinding', () => {
|
||||
const table: TC[] = [
|
||||
{
|
||||
title: 'full command',
|
||||
command: '/jira issue create --project P --summary = "SUM MA RY" --verbose --epic=epic2',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('--project');
|
||||
expect(parsed.incompleteStart).toBe(19);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'full command case insensitive',
|
||||
command: '/JiRa IsSuE CrEaTe --PrOjEcT P --SuMmArY = "SUM MA RY" --VeRbOsE --EpIc=epic2',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('--PrOjEcT');
|
||||
expect(parsed.incompleteStart).toBe(19);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'incomplete top command',
|
||||
command: '/jir',
|
||||
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
submit: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
},
|
||||
{
|
||||
title: 'no space after the top command',
|
||||
command: '/jira',
|
||||
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'space after the top command',
|
||||
command: '/jira ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'middle of subcommand',
|
||||
command: '/jira iss',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('iss');
|
||||
expect(parsed.incompleteStart).toBe(9);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('iss');
|
||||
expect(parsed.incompleteStart).toBe(9);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'second subcommand, no space',
|
||||
command: '/jira issue',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('issue');
|
||||
expect(parsed.incompleteStart).toBe(6);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('issue');
|
||||
expect(parsed.location).toBe('/jira/issue');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'token after the end of bindings, no space',
|
||||
command: '/jira issue create something',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'token after the end of bindings, with space',
|
||||
command: '/jira issue create something ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
},
|
||||
];
|
||||
|
||||
table.forEach((tc) => {
|
||||
test(tc.title, async () => {
|
||||
const bindings = testBindings[0].bindings as AppBinding[];
|
||||
|
||||
let a = new ParsedCommand(tc.command, parser, intl);
|
||||
a = await a.matchBinding(bindings, true);
|
||||
checkResult(a, tc.autocomplete || tc.submit);
|
||||
|
||||
let s = new ParsedCommand(tc.command, parser, intl);
|
||||
s = await s.matchBinding(bindings, false);
|
||||
checkResult(s, tc.submit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseForm', () => {
|
||||
const table: TC[] = [
|
||||
{
|
||||
title: 'happy full create',
|
||||
command: '/jira issue create --project `P 1` --summary "SUM MA RY" --verbose --epic=epic2',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy full create case insensitive',
|
||||
command: '/JiRa IsSuE CrEaTe --PrOjEcT `P 1` --SuMmArY "SUM MA RY" --VeRbOsE --EpIc=epic2',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'partial epic',
|
||||
command: '/jira issue create --project KT --summary "great feature" --epic M',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('M');
|
||||
expect(parsed.incompleteStart).toBe(65);
|
||||
expect(parsed.values?.project).toBe('KT');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.epic).toBe('M');
|
||||
}},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'happy full view',
|
||||
command: '/jira issue view --project=`P 1` MM-123',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('MM-123');
|
||||
expect(parsed.incompleteStart).toBe(33);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe('MM-123');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy view no parameters',
|
||||
command: '/jira issue view ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.StartParameter);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.incompleteStart).toBe(17);
|
||||
expect(parsed.values).toEqual({});
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy create flag no value',
|
||||
command: '/jira issue create --summary ',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.FlagValueSeparator);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({});
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({
|
||||
summary: '',
|
||||
});
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'error: unmatched tick',
|
||||
command: '/jira issue view --project `P 1',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.TickValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {expectError: 'Matching tick quote expected before end of input.'},
|
||||
},
|
||||
{
|
||||
title: 'error: unmatched quote',
|
||||
command: '/jira issue view --project "P \\1',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.QuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {expectError: 'Matching double quote expected before end of input.'},
|
||||
},
|
||||
{
|
||||
title: 'missing required fields not a problem for parseCommand',
|
||||
command: '/jira issue view --project "P 1"',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'error: invalid flag',
|
||||
command: '/jira issue view --wrong test',
|
||||
submit: {expectError: 'Command does not accept flag `{flagName}`.'},
|
||||
},
|
||||
{
|
||||
title: 'error: unexpected positional',
|
||||
command: '/jira issue create wrong',
|
||||
submit: {expectError: 'Unable to identify argument.'},
|
||||
},
|
||||
{
|
||||
title: 'error: multiple equal signs',
|
||||
command: '/jira issue create --project == test',
|
||||
submit: {expectError: 'Multiple `=` signs are not allowed.'},
|
||||
},
|
||||
];
|
||||
|
||||
table.forEach((tc) => {
|
||||
test(tc.title, async () => {
|
||||
const bindings = testBindings[0].bindings as AppBinding[];
|
||||
|
||||
let a = new ParsedCommand(tc.command, parser, intl);
|
||||
a = await a.matchBinding(bindings, true);
|
||||
a = a.parseForm(true);
|
||||
checkResult(a, tc.autocomplete || tc.submit);
|
||||
|
||||
let s = new ParsedCommand(tc.command, parser, intl);
|
||||
s = await s.matchBinding(bindings, false);
|
||||
s = s.parseForm(false);
|
||||
checkResult(s, tc.submit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuggestions', () => {
|
||||
test('subcommand 1', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'jira issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 1 case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'JiRa issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'jira issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'JiRa issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 with a space', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'view',
|
||||
Complete: 'jira issue view',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Description: 'View details of a Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'jira issue create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 with a space case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'view',
|
||||
Complete: 'JiRa IsSuE view',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Description: 'View details of a Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'JiRa IsSuE create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 3 partial', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue c');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'jira issue create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 3 partial case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE C');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'JiRa IsSuE create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('view just after subcommand (positional)', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue view ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view',
|
||||
Description: 'The Jira issue key',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Suggestion: 'issue: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('view flags just after subcommand', async () => {
|
||||
let suggestions = await parser.getSuggestions('/jira issue view -');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: '',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue view --');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: '',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flags just after subcommand', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create ');
|
||||
|
||||
let executeCommand: AutocompleteSuggestion[] = [];
|
||||
if (checkForExecuteSuggestion) {
|
||||
executeCommand = [
|
||||
{
|
||||
Complete: 'jira issue create _execute_current_command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
Hint: '',
|
||||
IconData: '_execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
...executeCommand,
|
||||
{
|
||||
Complete: 'jira issue create --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --verbose',
|
||||
Description: 'display details',
|
||||
Hint: 'yes or no!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--verbose',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--epic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('used flags do not appear', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT ');
|
||||
|
||||
let executeCommand: AutocompleteSuggestion[] = [];
|
||||
if (checkForExecuteSuggestion) {
|
||||
executeCommand = [
|
||||
{
|
||||
Complete: 'jira issue create --project KT _execute_current_command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
Hint: '',
|
||||
IconData: '_execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
...executeCommand,
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --verbose',
|
||||
Description: 'display details',
|
||||
Hint: 'yes or no!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--verbose',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--epic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flags mid-flag', async () => {
|
||||
const mid = await parser.getSuggestions('/jira issue create --project KT --summ');
|
||||
expect(mid).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
]);
|
||||
|
||||
const full = await parser.getSuggestions('/jira issue create --project KT --summary');
|
||||
expect(full).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('empty text value suggestion', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('partial text value suggestion', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary Sum',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: "Sum"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('quote text value suggestion close quotes', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "Sum"',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: "Sum"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('tick text value suggestion close quotes', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary `Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary `Sum`',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: `Sum`',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag summary value', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --summary ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag project dynamic select value', async () => {
|
||||
const f = Client4.executeAppCall;
|
||||
Client4.executeAppCall = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
|
||||
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project ');
|
||||
Client4.executeAppCall = f;
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project special-value',
|
||||
Suggestion: 'special-value',
|
||||
Description: 'special-label',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag epic static select value', async () => {
|
||||
let suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic1',
|
||||
Suggestion: 'Dylan Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
|
||||
Suggestion: 'Michael Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic M');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
|
||||
Suggestion: 'Michael Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic Nope');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic',
|
||||
Suggestion: '',
|
||||
Description: '',
|
||||
Hint: 'No matching options.',
|
||||
IconData: 'error',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('filled out form shows execute', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true ');
|
||||
|
||||
if (!checkForExecuteSuggestion) {
|
||||
expect(suggestions).toEqual([]);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true _execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
IconData: '_execute_current_command',
|
||||
Hint: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeCallFromCommand', () => {
|
||||
const base = {
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
channel_id: 'current_channel_id',
|
||||
location: '/command',
|
||||
root_id: 'root_id',
|
||||
team_id: 'team_id',
|
||||
},
|
||||
path: '/create-issue',
|
||||
};
|
||||
|
||||
test('empty form', async () => {
|
||||
const cmd = '/jira issue create';
|
||||
const values = {};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
...base,
|
||||
raw_command: cmd,
|
||||
expand: {},
|
||||
query: undefined,
|
||||
selected_field: undefined,
|
||||
values,
|
||||
});
|
||||
});
|
||||
|
||||
test('full form', async () => {
|
||||
const cmd = '/jira issue create --summary "Here it is" --epic epic1 --verbose true --project';
|
||||
const values = {
|
||||
summary: 'Here it is',
|
||||
epic: {
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
verbose: 'true',
|
||||
project: '',
|
||||
};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
...base,
|
||||
expand: {},
|
||||
selected_field: undefined,
|
||||
query: undefined,
|
||||
raw_command: cmd,
|
||||
values,
|
||||
});
|
||||
});
|
||||
|
||||
test('dynamic lookup test', async () => {
|
||||
const f = Client4.executeAppCall;
|
||||
|
||||
const mockedExecute = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
|
||||
Client4.executeAppCall = mockedExecute;
|
||||
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --summary "The summary" --epic epic1 --project special');
|
||||
Client4.executeAppCall = f;
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --summary "The summary" --epic epic1 --project special-value',
|
||||
Suggestion: 'special-value',
|
||||
Description: 'special-label',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockedExecute).toHaveBeenCalledWith({
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
channel_id: 'current_channel_id',
|
||||
location: '/command',
|
||||
root_id: 'root_id',
|
||||
team_id: 'team_id',
|
||||
},
|
||||
expand: {},
|
||||
path: '/create-issue',
|
||||
query: 'special',
|
||||
raw_command: '/jira issue create --summary "The summary" --epic epic1 --project special',
|
||||
selected_field: 'project',
|
||||
values: {
|
||||
summary: 'The summary',
|
||||
epic: {
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
},
|
||||
}, AppCallTypes.LOOKUP);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type {
|
||||
AppCallRequest,
|
||||
AppBinding,
|
||||
AppField,
|
||||
AppSelectOption,
|
||||
AppCallResponse,
|
||||
AppCallValues,
|
||||
AppContext,
|
||||
AppForm,
|
||||
AutocompleteElement,
|
||||
AutocompleteDynamicSelect,
|
||||
AutocompleteStaticSelect,
|
||||
AutocompleteUserSelect,
|
||||
AutocompleteChannelSelect,
|
||||
} from '@mm-redux/types/apps';
|
||||
|
||||
import type {
|
||||
AutocompleteSuggestion,
|
||||
} from '@mm-redux/types/integrations';
|
||||
export type {AutocompleteSuggestion};
|
||||
|
||||
export type {
|
||||
Channel,
|
||||
} from '@mm-redux/types/channels';
|
||||
|
||||
export type {
|
||||
GlobalState,
|
||||
} from '@mm-redux/types/store';
|
||||
|
||||
export type {
|
||||
DispatchFunc,
|
||||
} from '@mm-redux/types/actions';
|
||||
|
||||
export {
|
||||
AppBindingLocations,
|
||||
AppCallTypes,
|
||||
AppFieldTypes,
|
||||
AppCallResponseTypes,
|
||||
} from '@mm-redux/constants/apps';
|
||||
|
||||
export {getAppsBindings} from '@mm-redux/selectors/entities/apps';
|
||||
export {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
export {getChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
|
||||
export {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
export {getUserByUsername as selectUserByUsername} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
export {getUserByUsername} from '@mm-redux/actions/users';
|
||||
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
|
||||
|
||||
export {doAppCall} from '@actions/apps';
|
||||
export {createCallRequest} from '@utils/apps';
|
||||
|
||||
import Store from '@store/store';
|
||||
export const getStore = () => Store.redux;
|
||||
|
||||
import keyMirror from '@mm-redux/utils/key_mirror';
|
||||
export {keyMirror};
|
||||
|
||||
export const EXECUTE_CURRENT_COMMAND_ITEM_ID = '_execute_current_command';
|
||||
|
||||
import type {ParsedCommand} from './app_command_parser';
|
||||
export const getExecuteSuggestion = (_: ParsedCommand): AutocompleteSuggestion | null => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return null;
|
||||
};
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
export const displayError = (intl: typeof intlShape, body: string) => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.general.error.title',
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
Alert.alert(title, body);
|
||||
};
|
||||
|
||||
export const errorMessage = (intl: typeof intlShape, error: string, _command: string, _position: number): string => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return intl.formatMessage({
|
||||
id: 'apps.error.parser',
|
||||
defaultMessage: 'Parsing error: {error}',
|
||||
}, {
|
||||
error,
|
||||
});
|
||||
};
|
||||
@@ -1,228 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
AppBinding,
|
||||
AppForm,
|
||||
AppFieldTypes,
|
||||
} from './app_command_parser_test_dependencies';
|
||||
|
||||
export const reduxTestState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'current_channel_id',
|
||||
myMembers: {
|
||||
current_channel_id: {
|
||||
channel_id: 'current_channel_id',
|
||||
user_id: 'current_user_id',
|
||||
roles: 'channel_role',
|
||||
mention_count: 1,
|
||||
msg_count: 9,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
current_channel_id: {
|
||||
id: 'current_channel_id',
|
||||
name: 'default-name',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
total_msg_count: 10,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
current_user_id__existingId: {
|
||||
id: 'current_user_id__existingId',
|
||||
name: 'current_user_id__existingId',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: '0',
|
||||
total_msg_count: 0,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
},
|
||||
channelsInTeam: {
|
||||
'team-id': ['current_channel_id'],
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team_id',
|
||||
name: 'team-1',
|
||||
displayName: 'Team 1',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
'team-id': {roles: 'team_role'},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
'display_settings--name_format': {
|
||||
category: 'display_settings',
|
||||
name: 'name_format',
|
||||
user_id: 'current_user_id',
|
||||
value: 'username',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {
|
||||
permissions: [],
|
||||
},
|
||||
team_role: {
|
||||
permissions: [],
|
||||
},
|
||||
channel_role: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
license: {IsLicensed: 'false'},
|
||||
serverVersion: '5.25.0',
|
||||
config: {PostEditTimeLimit: -1},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const viewCommand: AppBinding = {
|
||||
app_id: 'jira',
|
||||
label: 'view',
|
||||
location: 'view',
|
||||
description: 'View details of a Jira issue',
|
||||
form: {
|
||||
call: {
|
||||
path: '/view-issue',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'project',
|
||||
label: 'project',
|
||||
description: 'The Jira project description',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
is_required: true,
|
||||
},
|
||||
{
|
||||
name: 'issue',
|
||||
position: 1,
|
||||
description: 'The Jira issue key',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'MM-11343',
|
||||
is_required: true,
|
||||
},
|
||||
],
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const createCommand: AppBinding = {
|
||||
app_id: 'jira',
|
||||
label: 'create',
|
||||
location: 'create',
|
||||
description: 'Create a new Jira issue',
|
||||
icon: 'Create icon',
|
||||
hint: 'Create hint',
|
||||
form: {
|
||||
call: {
|
||||
path: '/create-issue',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'project',
|
||||
label: 'project',
|
||||
description: 'The Jira project description',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
},
|
||||
{
|
||||
name: 'summary',
|
||||
label: 'summary',
|
||||
description: 'The Jira issue summary',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'The thing is working great!',
|
||||
},
|
||||
{
|
||||
name: 'verbose',
|
||||
label: 'verbose',
|
||||
description: 'display details',
|
||||
type: AppFieldTypes.BOOL,
|
||||
hint: 'yes or no!',
|
||||
},
|
||||
{
|
||||
name: 'epic',
|
||||
label: 'epic',
|
||||
description: 'The Jira epic',
|
||||
type: AppFieldTypes.STATIC_SELECT,
|
||||
hint: 'The thing is working great!',
|
||||
options: [
|
||||
{
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
{
|
||||
label: 'Michael Epic',
|
||||
value: 'epic2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const testBindings: AppBinding[] = [
|
||||
{
|
||||
app_id: '',
|
||||
label: '',
|
||||
location: '/command',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'jira',
|
||||
label: 'jira',
|
||||
description: 'Interact with your Jira instance',
|
||||
icon: 'Jira icon',
|
||||
hint: 'Jira hint',
|
||||
bindings: [{
|
||||
app_id: 'jira',
|
||||
label: 'issue',
|
||||
description: 'Interact with Jira issues',
|
||||
icon: 'Issue icon',
|
||||
hint: 'Issue hint',
|
||||
bindings: [
|
||||
viewCommand,
|
||||
createCommand,
|
||||
],
|
||||
}],
|
||||
},
|
||||
{
|
||||
app_id: 'other',
|
||||
label: 'other',
|
||||
description: 'Other description',
|
||||
icon: 'Other icon',
|
||||
hint: 'Other hint',
|
||||
bindings: [{
|
||||
app_id: 'other',
|
||||
label: 'sub1',
|
||||
description: 'Some Description',
|
||||
form: {
|
||||
fields: [{
|
||||
name: 'fieldname',
|
||||
label: 'fieldlabel',
|
||||
description: 'field description',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'field hint',
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import thunk from 'redux-thunk';
|
||||
export {thunk};
|
||||
|
||||
const configureStore = require('redux-mock-store').default;
|
||||
export {configureStore};
|
||||
|
||||
export {Client4} from '@client/rest';
|
||||
|
||||
export type {AppBinding, AppForm} from '@mm-redux/types/apps';
|
||||
export {AppFieldTypes} from '@mm-redux/constants/apps';
|
||||
|
||||
export const checkForExecuteSuggestion = false;
|
||||
@@ -1,18 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-redux/actions/integrations';
|
||||
import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
// TODO: Remove when all below commands have been implemented
|
||||
@@ -28,17 +25,16 @@ const mobileCommandsSelector = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
commands: mobileCommandsSelector(state),
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state),
|
||||
suggestions: getCommandAutocompleteSuggestionsList(state),
|
||||
appsEnabled: appsEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getAutocompleteCommands,
|
||||
@@ -1,83 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Client4} from '@client/rest';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {Command, AutocompleteSuggestion, CommandArgs} from '@mm-redux/types/integrations';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
|
||||
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
|
||||
|
||||
export type Props = {
|
||||
actions: {
|
||||
getAutocompleteCommands: (channelID: string) => void;
|
||||
getCommandAutocompleteSuggestions: (value: string, teamID: string, args: CommandArgs) => void;
|
||||
export default class SlashSuggestion extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getAutocompleteCommands: PropTypes.func.isRequired,
|
||||
getCommandAutocompleteSuggestions: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
commands: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
maxListHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
suggestions: PropTypes.array,
|
||||
rootId: PropTypes.string,
|
||||
channelId: PropTypes.string,
|
||||
};
|
||||
currentTeamId: string;
|
||||
commands: Command[];
|
||||
isSearch?: boolean;
|
||||
maxListHeight?: number;
|
||||
theme: Theme;
|
||||
onChangeText: (text: string) => void;
|
||||
onResultCountChange: (count: number) => void;
|
||||
value: string;
|
||||
nestedScrollEnabled?: boolean;
|
||||
suggestions: AutocompleteSuggestion[];
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
appsEnabled: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
active: boolean;
|
||||
dataSource: AutocompleteSuggestion[];
|
||||
lastCommandRequest: number;
|
||||
}
|
||||
|
||||
export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
value: '',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
appCommandParser: AppCommandParser;
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
dataSource: [],
|
||||
lastCommandRequest: 0,
|
||||
};
|
||||
|
||||
constructor(props: Props, context: any) {
|
||||
super(props);
|
||||
this.appCommandParser = new AppCommandParser(null, context.intl, props.channelId, props.rootId);
|
||||
}
|
||||
|
||||
setActive(active: boolean) {
|
||||
setActive(active) {
|
||||
this.setState({active});
|
||||
}
|
||||
|
||||
setLastCommandRequest(lastCommandRequest: number) {
|
||||
setLastCommandRequest(lastCommandRequest) {
|
||||
this.setState({lastCommandRequest});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate(prevProps) {
|
||||
if ((this.props.value === prevProps.value && this.props.suggestions === prevProps.suggestions && this.props.commands === prevProps.commands) ||
|
||||
this.props.isSearch || this.props.value.startsWith('//') || !this.props.channelId) {
|
||||
return;
|
||||
@@ -109,23 +88,25 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
this.setLastCommandRequest(Date.now());
|
||||
}
|
||||
|
||||
this.showBaseCommands(nextValue, nextCommands, prevProps.channelId, prevProps.rootId);
|
||||
const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
|
||||
this.updateSuggestions(matches);
|
||||
} else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
|
||||
// If this is an app command, then hand it off to the app command parser.
|
||||
if (this.props.appsEnabled && this.isAppCommand(nextValue, prevProps.channelId, prevProps.rootId)) {
|
||||
this.fetchAndShowAppCommandSuggestions(nextValue, prevProps.channelId, prevProps.rootId);
|
||||
} else if (nextSuggestions === prevProps.suggestions) {
|
||||
if (nextSuggestions === prevProps.suggestions) {
|
||||
const args = {
|
||||
channel_id: prevProps.channelId,
|
||||
team_id: prevProps.currentTeamId,
|
||||
...(prevProps.rootId && {root_id: prevProps.rootId, parent_id: prevProps.rootId}),
|
||||
};
|
||||
this.props.actions.getCommandAutocompleteSuggestions(nextValue, nextTeamId, args);
|
||||
} else {
|
||||
const matches: AutocompleteSuggestion[] = [];
|
||||
nextSuggestions.forEach((suggestion: AutocompleteSuggestion) => {
|
||||
if (!this.contains(matches, '/' + suggestion.Complete)) {
|
||||
matches.push(suggestion);
|
||||
const matches = [];
|
||||
nextSuggestions.forEach((sug) => {
|
||||
if (!this.contains(matches, '/' + sug.Complete)) {
|
||||
matches.push({
|
||||
Complete: sug.Complete,
|
||||
Suggestion: sug.Suggestion,
|
||||
Hint: sug.Hint,
|
||||
Description: sug.Description,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.updateSuggestions(matches);
|
||||
@@ -135,52 +116,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
showBaseCommands = (text: string, commands: Command[], channelID: string, rootID?: string) => {
|
||||
let matches: AutocompleteSuggestion[] = [];
|
||||
|
||||
if (this.props.appsEnabled) {
|
||||
const appCommands = this.getAppBaseCommandSuggestions(text, channelID, rootID);
|
||||
matches = matches.concat(appCommands);
|
||||
}
|
||||
|
||||
matches = matches.concat(this.filterCommands(text.substring(1), commands));
|
||||
|
||||
matches.sort((match1, match2) => {
|
||||
if (match1.Suggestion === match2.Suggestion) {
|
||||
return 0;
|
||||
}
|
||||
return match1.Suggestion > match2.Suggestion ? 1 : -1;
|
||||
});
|
||||
|
||||
this.updateSuggestions(matches);
|
||||
}
|
||||
|
||||
isAppCommand = (pretext: string, channelID: string, rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
return this.appCommandParser.isAppCommand(pretext);
|
||||
}
|
||||
|
||||
fetchAndShowAppCommandSuggestions = async (pretext: string, channelID: string, rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
const suggestions = await this.appCommandParser.getSuggestions(pretext);
|
||||
this.updateSuggestions(suggestions);
|
||||
}
|
||||
|
||||
getAppBaseCommandSuggestions = (pretext: string, channelID: string, rootID?: string): AutocompleteSuggestion[] => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
const suggestions = this.appCommandParser.getSuggestionsBase(pretext);
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
updateSuggestions = (matches: AutocompleteSuggestion[]) => {
|
||||
updateSuggestions = (matches) => {
|
||||
this.setState({
|
||||
active: Boolean(matches.length),
|
||||
active: matches.length,
|
||||
dataSource: matches,
|
||||
});
|
||||
this.props.onResultCountChange(matches.length);
|
||||
}
|
||||
|
||||
filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
|
||||
filterSlashSuggestions = (matchTerm, commands) => {
|
||||
const data = commands.filter((command) => {
|
||||
if (!command.auto_complete) {
|
||||
return false;
|
||||
@@ -196,16 +140,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
Suggestion: '/' + item.trigger,
|
||||
Hint: item.auto_complete_hint,
|
||||
Description: item.auto_complete_desc,
|
||||
IconData: item.icon_url,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
contains = (matches: AutocompleteSuggestion[], complete: string): boolean => {
|
||||
return matches.findIndex((match) => match.Complete === complete) !== -1;
|
||||
contains = (matches, complete) => {
|
||||
return matches.findIndex((match) => match.complete === complete) !== -1;
|
||||
}
|
||||
|
||||
completeSuggestion = (command: string) => {
|
||||
completeSuggestion = (command) => {
|
||||
const {onChangeText} = this.props;
|
||||
analytics.trackCommand('complete_suggestion', `/${command} `);
|
||||
|
||||
@@ -233,9 +176,9 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id || item.Suggestion;
|
||||
keyExtractor = (item) => item.id || item.Suggestion;
|
||||
|
||||
renderItem = ({item}: {item: AutocompleteSuggestion}) => (
|
||||
renderItem = ({item}) => (
|
||||
<SlashSuggestionItem
|
||||
description={item.Description}
|
||||
hint={item.Hint}
|
||||
@@ -243,7 +186,6 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
theme={this.props.theme}
|
||||
suggestion={item.Suggestion}
|
||||
complete={item.Complete}
|
||||
icon={item.IconData}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -266,15 +208,16 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
@@ -1,281 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {Command, AutocompleteSuggestion} from '@mm-redux/types/integrations';
|
||||
import Store from '@store/store';
|
||||
import {intl} from 'test/intl-test-helper';
|
||||
|
||||
import {
|
||||
thunk,
|
||||
configureStore,
|
||||
Client4,
|
||||
AppBinding,
|
||||
} from './app_command_parser/tests/app_command_parser_test_dependencies';
|
||||
|
||||
import {
|
||||
reduxTestState,
|
||||
testBindings,
|
||||
} from './app_command_parser/tests/app_command_parser_test_data';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
const makeStore = async (bindings: AppBinding[]) => {
|
||||
const initialState = {
|
||||
...reduxTestState,
|
||||
entities: {
|
||||
...reduxTestState.entities,
|
||||
apps: {bindings},
|
||||
},
|
||||
} as any;
|
||||
const testStore = await mockStore(initialState);
|
||||
|
||||
return testStore;
|
||||
};
|
||||
|
||||
import SlashSuggestion, {Props} from './slash_suggestion';
|
||||
|
||||
describe('components/autocomplete/slash_suggestion', () => {
|
||||
const sampleCommand = {
|
||||
trigger: 'jitsi',
|
||||
auto_complete: true,
|
||||
auto_complete_desc: 'The Jitsi Description',
|
||||
auto_complete_hint: 'The Jitsi Hint',
|
||||
display_name: 'The Jitsi Display Name',
|
||||
icon_url: 'Jitsi icon',
|
||||
} as Command;
|
||||
|
||||
const baseProps: Props = {
|
||||
actions: {
|
||||
getAutocompleteCommands: jest.fn(),
|
||||
getCommandAutocompleteSuggestions: jest.fn(),
|
||||
},
|
||||
currentTeamId: '',
|
||||
commands: [sampleCommand],
|
||||
isSearch: false,
|
||||
maxListHeight: 50,
|
||||
theme: Preferences.THEMES.default,
|
||||
onChangeText: jest.fn(),
|
||||
onResultCountChange: jest.fn(),
|
||||
value: '',
|
||||
nestedScrollEnabled: false,
|
||||
suggestions: [],
|
||||
rootId: '',
|
||||
channelId: 'thechannel',
|
||||
appsEnabled: true,
|
||||
};
|
||||
|
||||
const f = Client4.getServerVersion;
|
||||
|
||||
beforeAll(async () => {
|
||||
Client4.getServerVersion = jest.fn().mockReturnValue('5.30.0');
|
||||
|
||||
const store = await makeStore(testBindings);
|
||||
Store.redux = store;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Client4.getServerVersion = f;
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
|
||||
const dataSource: AutocompleteSuggestion[] = [
|
||||
{
|
||||
Complete: 'thetrigger',
|
||||
Description: 'The Description',
|
||||
Hint: 'The Hint',
|
||||
IconData: 'iconurl.com',
|
||||
Suggestion: '/thetrigger',
|
||||
},
|
||||
];
|
||||
wrapper.setState({active: true, dataSource, lastCommandRequest: 1234});
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show commands from props.commands', async () => {
|
||||
const command = {
|
||||
trigger: 'thetrigger',
|
||||
auto_complete: true,
|
||||
auto_complete_desc: 'The Description',
|
||||
auto_complete_hint: 'The Hint',
|
||||
display_name: 'The Display Name',
|
||||
icon_url: 'iconurl.com',
|
||||
} as Command;
|
||||
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
commands: [command],
|
||||
};
|
||||
|
||||
const wrapper = shallow<SlashSuggestion>(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
wrapper.setProps({value: '/the'});
|
||||
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'thetrigger',
|
||||
Description: 'The Description',
|
||||
Hint: 'The Hint',
|
||||
IconData: 'iconurl.com',
|
||||
Suggestion: '/thetrigger',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should show commands from app base commands', async () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
commands: [],
|
||||
};
|
||||
|
||||
const wrapper = shallow<SlashSuggestion>(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
wrapper.setProps({value: '/ji'});
|
||||
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'jira',
|
||||
Description: 'Interact with your Jira instance',
|
||||
Hint: 'Jira hint',
|
||||
IconData: 'Jira icon',
|
||||
Suggestion: '/jira',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should show commands from app base commands and regular commands', async () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
};
|
||||
|
||||
const wrapper = shallow<SlashSuggestion>(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
|
||||
wrapper.setProps({value: '/'});
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'jira',
|
||||
Description: 'Interact with your Jira instance',
|
||||
Hint: 'Jira hint',
|
||||
IconData: 'Jira icon',
|
||||
Suggestion: '/jira',
|
||||
},
|
||||
{
|
||||
Complete: 'jitsi',
|
||||
Description: 'The Jitsi Description',
|
||||
Hint: 'The Jitsi Hint',
|
||||
IconData: 'Jitsi icon',
|
||||
Suggestion: '/jitsi',
|
||||
},
|
||||
{
|
||||
Complete: 'other',
|
||||
Description: 'Other description',
|
||||
Hint: 'Other hint',
|
||||
IconData: 'Other icon',
|
||||
Suggestion: '/other',
|
||||
},
|
||||
]);
|
||||
|
||||
wrapper.setProps({value: '/ji'});
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'jira',
|
||||
Description: 'Interact with your Jira instance',
|
||||
Hint: 'Jira hint',
|
||||
IconData: 'Jira icon',
|
||||
Suggestion: '/jira',
|
||||
},
|
||||
{
|
||||
Complete: 'jitsi',
|
||||
Description: 'The Jitsi Description',
|
||||
Hint: 'The Jitsi Hint',
|
||||
IconData: 'Jitsi icon',
|
||||
Suggestion: '/jitsi',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should show commands from app sub commands', async (done) => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
};
|
||||
|
||||
const wrapper = shallow<SlashSuggestion>(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
wrapper.setProps({value: '/jira i', suggestions: []});
|
||||
|
||||
const expected: AutocompleteSuggestion[] = [
|
||||
{
|
||||
Complete: 'jira issue',
|
||||
Description: 'Interact with Jira issues',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Suggestion: 'issue',
|
||||
},
|
||||
];
|
||||
|
||||
setTimeout(() => {
|
||||
expect(wrapper.state('dataSource')).toEqual(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('should avoid using app commands when apps are disabled', async () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
appsEnabled: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow<SlashSuggestion>(
|
||||
<SlashSuggestion {...props}/>,
|
||||
{context: {intl}},
|
||||
);
|
||||
wrapper.setProps({value: '/', suggestions: []});
|
||||
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'jitsi',
|
||||
Description: 'The Jitsi Description',
|
||||
Hint: 'The Jitsi Hint',
|
||||
IconData: 'Jitsi icon',
|
||||
Suggestion: '/jitsi',
|
||||
},
|
||||
]);
|
||||
|
||||
wrapper.setProps({value: '/ji', suggestions: []});
|
||||
|
||||
expect(wrapper.state('dataSource')).toEqual([
|
||||
{
|
||||
Complete: 'jitsi',
|
||||
Description: 'The Jitsi Description',
|
||||
Hint: 'The Jitsi Hint',
|
||||
IconData: 'Jitsi icon',
|
||||
Suggestion: '/jitsi',
|
||||
},
|
||||
]);
|
||||
|
||||
wrapper.setProps({value: '/jira i', suggestions: []});
|
||||
expect(wrapper.state('dataSource')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user