Compare commits

..

10 Commits

Author SHA1 Message Date
Elias Nahum
ce283324ae Bump app version number to 1.41.1 2021-04-07 14:38:41 -04:00
Elias Nahum
3e54975684 Bump app build number to 349 (#5291) 2021-04-07 14:36:39 -04:00
Mattermost Build
b14dafa57e MM-34508 in-app browser emm configuration (#5274) (#5289)
(cherry picked from commit 66ebe3683b)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2021-04-07 14:21:42 -04:00
Mattermost Build
121656038c Bump app build number to 347 (#5224) (#5225)
(cherry picked from commit 09386d9fde)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-03-15 10:34:44 -07:00
Weblate (bot)
064b1883ce Translations update from Weblate (#5223)
* Translated using Weblate (Spanish)

Currently translated at 99.5% (659 of 662 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (662 of 662 strings)

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

* Translated using Weblate (Korean)

Currently translated at 97.2% (644 of 662 strings)

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

Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Ji-Hyeon Gim <potatogim@potatogim.net>
2021-03-15 12:11:41 +01:00
Mattermost Build
1282ff1e8e Disable check deps CI job (#5214) (#5216)
(cherry picked from commit 5f6552a649)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-03-08 16:02:36 -03:00
Mattermost Build
6adbc03faa MM-33495 Sanitize filename in Android ShareExtension (#5210) (#5215)
* MM-33495 Sanitize filename in Android ShareExtension

* Apply sanitization after getting uri last path segment

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
(cherry picked from commit 86a096d1ce)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-03-08 15:36:06 -03:00
Mattermost Build
836dc521b4 Fix deployment of iOS to TestFlight (#5208) (#5209)
* use API Key for iOS build & deployment

* fix api key file path

* iOS API key from p8 file instead of json

* Properly split api key on newlines

(cherry picked from commit d68cc9c5e1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-03-05 15:43:50 -03:00
Mattermost Build
8a3eb36911 Bump app build number to 346 (#5205) (#5206)
(cherry picked from commit 3404495133)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-03-03 20:09:22 -07:00
Mattermost Build
a7dfc99cf6 Bump app version number to 1.41.0 (#5203) (#5204)
(cherry picked from commit c416343388)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-03-03 20:01:21 -07:00
955 changed files with 46532 additions and 47749 deletions

View File

@@ -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

View File

@@ -6,8 +6,8 @@
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"mattermost"
"mattermost",
"@typescript-eslint"
],
"settings": {
"react": {

View File

@@ -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
View File

@@ -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

View File

@@ -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 youre filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://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

View File

@@ -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
View File

@@ -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
View File

@@ -1 +0,0 @@
_

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
sh ./scripts/pre-commit.sh

View File

@@ -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.
---

View File

@@ -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+

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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"));
}

View File

@@ -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>

View File

@@ -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()

View File

@@ -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

Binary file not shown.

View File

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

22
android/gradlew.bat vendored
View File

@@ -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

View File

@@ -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,
));
};
}

View File

@@ -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,
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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},
},

View File

@@ -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);
});

View File

@@ -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};

View 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,
};
}

View 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};
};
}

View File

@@ -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, '--');
};

View File

@@ -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,
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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 {};
};
}

View File

@@ -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,
},
};
}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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};
};
}

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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

View File

@@ -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',
];

View File

@@ -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;

View File

@@ -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]);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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});
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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={

View File

@@ -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 (

View File

@@ -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', () => {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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}
/>
);
}

View File

@@ -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>
);

View File

@@ -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,
};

View File

@@ -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}
/>
);
}

View File

@@ -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),
};

View File

@@ -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}>

View File

@@ -30,7 +30,6 @@ function mapStateToProps(state, ownProps) {
return {
displayName,
name: channel?.name,
shared: channel?.shared,
type: channel?.type,
isBot,
isGuest,

View File

@@ -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={

View File

@@ -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}

View File

@@ -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}
/>
`;

View File

@@ -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);
});
});
});

View File

@@ -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,
});
};

View File

@@ -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',
}],
},
}],
},
],
},
];

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -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