forked from Ivasoft/mattermost-mobile
Compare commits
7 Commits
release-1.
...
v1.41.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
121656038c | ||
|
|
064b1883ce | ||
|
|
1282ff1e8e | ||
|
|
6adbc03faa | ||
|
|
836dc521b4 | ||
|
|
8a3eb36911 | ||
|
|
a7dfc99cf6 |
@@ -23,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
|
||||
|
||||
@@ -550,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
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"mattermost"
|
||||
"mattermost",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
|
||||
13
.flowconfig
13
.flowconfig
@@ -8,6 +8,10 @@
|
||||
; Ignore polyfills
|
||||
node_modules/react-native/Libraries/polyfills/.*
|
||||
|
||||
; These should not be required directly
|
||||
; require from fbjs/lib instead: require('fbjs/lib/warning')
|
||||
node_modules/warning/.*
|
||||
|
||||
; Flow doesn't support platforms
|
||||
.*/Libraries/Utilities/LoadingView.js
|
||||
|
||||
@@ -26,8 +30,6 @@ emoji=true
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
@@ -42,6 +44,10 @@ suppress_type=$FlowFixMe
|
||||
suppress_type=$FlowFixMeProps
|
||||
suppress_type=$FlowFixMeState
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
|
||||
|
||||
[lints]
|
||||
sketchy-null-number=warn
|
||||
sketchy-null-mixed=warn
|
||||
@@ -53,6 +59,7 @@ unsafe-getters-setters=warn
|
||||
inexact-spread=warn
|
||||
unnecessary-invariant=warn
|
||||
signature-verification-failure=warn
|
||||
deprecated-utility=error
|
||||
|
||||
[strict]
|
||||
deprecated-type
|
||||
@@ -64,4 +71,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.137.0
|
||||
^0.122.0
|
||||
|
||||
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -1,3 +1,4 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
||||
*.pbxproj -text
|
||||
|
||||
# specific for windows script files
|
||||
*.bat text eol=crlf
|
||||
|
||||
32
.github/ISSUE_TEMPLATE.md
vendored
32
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,32 +0,0 @@
|
||||
Per Mattermost guidelines, GitHub issues are for bug reports: <http://www.mattermost.org/filing-issues/>.
|
||||
|
||||
For troubleshooting see: http://forum.mattermost.org/.
|
||||
For feature proposals see: http://www.mattermost.org/feature-requests/
|
||||
|
||||
If you've found a bug--something appears unintentional--please follow these steps:
|
||||
|
||||
1. Confirm you’re filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
|
||||
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://about.mattermost.com/report-security-issue/).
|
||||
3. [File a new issue](https://github.com/mattermost/mattermost-mobile/issues/new) using the format below. Mattermost will confirm steps to reproduce and file in Jira, or ask for more details if there is trouble reproducing it. If there's already an existing bug in Jira, it will be linked back to the GitHub issue so you can track when it gets fixed.
|
||||
|
||||
#### Summary
|
||||
Bug report in one concise sentence
|
||||
|
||||
### Environment Information
|
||||
- Device Name:
|
||||
- OS Version:
|
||||
- Mattermost App Version:
|
||||
- Mattermost Server Version:
|
||||
|
||||
#### Steps to reproduce
|
||||
How can we reproduce the issue (what version are you using?)
|
||||
|
||||
#### Expected behavior
|
||||
Describe your issue in detail
|
||||
|
||||
#### Observed behavior (that appears unintentional)
|
||||
What did you see happen? Please include relevant error messages, screenshots and/or video recordings.
|
||||
|
||||
#### Possible fixes
|
||||
If you can, link to the line of code that might be responsible for the problem
|
||||
|
||||
61
.github/PULL_REQUEST_TEMPLATE.md
vendored
61
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,61 +0,0 @@
|
||||
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
|
||||
|
||||
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
|
||||
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
|
||||
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
|
||||
-->
|
||||
|
||||
#### Summary
|
||||
<!--
|
||||
A brief description of what this pull request does.
|
||||
-->
|
||||
|
||||
#### Ticket Link
|
||||
<!--
|
||||
If this pull request addresses a Help Wanted ticket or fixes a reported issue, please link the relevant GitHub issue, e.g.
|
||||
|
||||
Fixes https://github.com/mattermost/mattermost-mobile/issues/XXXXX
|
||||
|
||||
Otherwise, link the JIRA ticket.
|
||||
-->
|
||||
|
||||
#### Checklist
|
||||
<!--
|
||||
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
|
||||
-->
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
|
||||
#### Screenshots
|
||||
<!--
|
||||
If the PR includes UI changes, include screenshots/GIFs/Videos (for both iOS and Android if possible).
|
||||
-->
|
||||
|
||||
#### Release Note
|
||||
<!--
|
||||
Add a release note for each of the following conditions:
|
||||
|
||||
* New features and improvements, including behavioural changes, UI changes
|
||||
* Bug fixes and fixes of previous known issues
|
||||
* Deprecation warnings, breaking changes, or compatibility notes
|
||||
|
||||
If no release notes are required write NONE. Use past-tense. Newlines are stripped.
|
||||
|
||||
Example:
|
||||
|
||||
```release-note
|
||||
Added a new config setting ServiceSettings.FooBar. Added a new column Foo to the Users table.
|
||||
```
|
||||
|
||||
```release-note
|
||||
NONE
|
||||
```
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
sh ./scripts/pre-commit.sh
|
||||
31
NOTICE.txt
31
NOTICE.txt
@@ -694,6 +694,37 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## core-js
|
||||
|
||||
This product contains 'core-js' by Denis Pushkarev.
|
||||
|
||||
Modular standard library for JavaScript.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/zloirock/core-js
|
||||
|
||||
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## deep-equal
|
||||
|
||||
This product contains 'deep-equal' by James Halliday.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.31.3)
|
||||
- **Minimum Server versions:** Current ESR version (5.25)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
||||
@@ -132,8 +132,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 356
|
||||
versionName "1.43.0"
|
||||
versionCode 347
|
||||
versionName "1.41.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -191,10 +191,6 @@ android {
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst '**/*.so'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
||||
@@ -4,10 +4,5 @@
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
</manifest>
|
||||
|
||||
@@ -50,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" />
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -72,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,4 @@
|
||||
<string name="timeout_description">How long in milliseconds the mobile app should wait for the server to respond.</string>
|
||||
<string name="vendor_title">EMM Vendor or Company Name</string>
|
||||
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
|
||||
<string name="inAppSessionAuth_title">In-App Session Auth</string>
|
||||
<string name="inAppSessionAuth_description">Instead of default flow from the mobile browser, enforce SSO with the WebView.</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
android:description="@string/inAppPinCode_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="inAppSessionAuth"
|
||||
android:title="@string/inAppSessionAuth_title"
|
||||
android:description="@string/inAppSessionAuth_description"
|
||||
android:restrictionType="string"
|
||||
android:defaultValue="false" />
|
||||
<restriction
|
||||
android:key="blurApplicationScreen"
|
||||
android:title="@string/blurApplicationScreen_title"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.75.1
|
||||
FLIPPER_VERSION=0.37.0
|
||||
|
||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
|
||||
22
android/gradlew.bat
vendored
22
android/gradlew.bat
vendored
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,14 +64,28 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
|
||||
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {showModal} from './navigation';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeCallErrorResponse} from '@utils/apps';
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
|
||||
switch (responseType) {
|
||||
case AppCallResponseTypes.OK:
|
||||
return {data: res};
|
||||
case AppCallResponseTypes.ERROR:
|
||||
return {error: res};
|
||||
case AppCallResponseTypes.FORM: {
|
||||
if (!res.form) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.form.no_form',
|
||||
defaultMessage: 'Response type is `form`, but no form was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
const screen = EphemeralStore.getNavigationTopComponentId();
|
||||
if (type === AppCallTypes.SUBMIT && screen !== 'AppForm') {
|
||||
showAppForm(res.form, call, getTheme(getState()));
|
||||
}
|
||||
|
||||
return {data: res};
|
||||
}
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
if (!res.navigate_to_url) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.navigate.no_url',
|
||||
defaultMessage: 'Response type is `navigate`, but no url was included in response.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
if (type !== AppCallTypes.SUBMIT) {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.navigate.no_submit',
|
||||
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
dispatch(handleGotoLocation(res.navigate_to_url, intl));
|
||||
|
||||
return {data: res};
|
||||
default: {
|
||||
const errMsg = intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: responseType,
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error.message || intl.formatMessage({
|
||||
id: 'apps.error.responses.unexpected_error',
|
||||
defaultMessage: 'Received an unexpected error.',
|
||||
});
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) => {
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
let submitButtons = [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: 'Submit',
|
||||
}];
|
||||
if (form.submit_buttons) {
|
||||
const options = form.fields.find((f) => f.name === form.submit_buttons)?.options;
|
||||
const newButtons = options?.map((o) => {
|
||||
return {
|
||||
id: 'submit-form_' + o.value,
|
||||
showAsAction: 'always',
|
||||
text: o.label,
|
||||
};
|
||||
});
|
||||
if (newButtons && newButtons.length > 0) {
|
||||
submitButtons = newButtons;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: submitButtons,
|
||||
},
|
||||
};
|
||||
|
||||
const passProps = {form, call};
|
||||
showModal('AppForm', form.title, passProps, options);
|
||||
};
|
||||
|
||||
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
post.channel_id,
|
||||
post.root_id || post.id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForChannel(response: AppCallResponse, message: string, channelID: string): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
channelID,
|
||||
'',
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForContext(response: AppCallResponse, message: string, context: AppContext): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
context.channel_id,
|
||||
context.root_id || context.post_id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
export function postEphemeralCallResponseForCommandArgs(response: AppCallResponse, message: string, args: CommandArgs): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
message,
|
||||
args.channel_id,
|
||||
args.root_id,
|
||||
response.app_metadata?.bot_user_id,
|
||||
));
|
||||
};
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
@@ -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';
|
||||
@@ -39,8 +39,6 @@ import {getChannelReachable} from '@selectors/channel';
|
||||
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,7 +185,6 @@ 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];
|
||||
@@ -214,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
|
||||
}
|
||||
|
||||
@@ -632,7 +625,7 @@ function setLoadMorePostsVisible(visible) {
|
||||
};
|
||||
}
|
||||
|
||||
function loadGroupData(isReconnect = false) {
|
||||
function loadGroupData() {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const actions = [];
|
||||
@@ -665,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) {
|
||||
@@ -714,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,
|
||||
@@ -726,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),
|
||||
]);
|
||||
|
||||
@@ -785,7 +777,7 @@ export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect =
|
||||
dispatch(loadUnreadChannelPosts(data.channels, data.channelMembers));
|
||||
}
|
||||
|
||||
dispatch(loadGroupData(isReconnect));
|
||||
dispatch(loadGroupData());
|
||||
}
|
||||
|
||||
return {data};
|
||||
|
||||
39
app/actions/views/command.js
Normal file
39
app/actions/views/command.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
export function executeCommand(message, channelId, rootId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
const args = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
if (cmdLength < 0) {
|
||||
cmdLength = msg.length;
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data?.trigger_id) { //eslint-disable-line camelcase
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
return {data, error};
|
||||
};
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
|
||||
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
|
||||
const teamId = getCurrentTeamId(state);
|
||||
|
||||
const args: CommandArgs = {
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
msg = filterEmDashForCommand(msg);
|
||||
|
||||
let cmdLength = msg.indexOf(' ');
|
||||
if (cmdLength < 0) {
|
||||
cmdLength = msg.length;
|
||||
}
|
||||
|
||||
const cmd = msg.substring(0, cmdLength).toLowerCase();
|
||||
msg = cmd + msg.substring(cmdLength, msg.length);
|
||||
|
||||
const appsAreEnabled = appsEnabled(state);
|
||||
if (appsAreEnabled) {
|
||||
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.root_id);
|
||||
if (parser.isAppCommand(msg)) {
|
||||
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
return {error: {message: errMessage}};
|
||||
};
|
||||
|
||||
if (!call) {
|
||||
return createErrorMessage(errorMessage!);
|
||||
}
|
||||
|
||||
const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
|
||||
if (res.error) {
|
||||
const errorResponse = res.error as AppCallResponse;
|
||||
return createErrorMessage(errorResponse.error || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error.',
|
||||
}));
|
||||
}
|
||||
const callResp = res.data as AppCallResponse;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.markdown) {
|
||||
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {data: {}};
|
||||
default:
|
||||
return createErrorMessage(intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const {data, error} = await dispatch(executeCommandService(msg, args));
|
||||
|
||||
if (data?.trigger_id) { //eslint-disable-line camelcase
|
||||
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
|
||||
}
|
||||
|
||||
return {data, error};
|
||||
};
|
||||
}
|
||||
|
||||
const filterEmDashForCommand = (command: string): string => {
|
||||
return command.replace(/\u2014/g, '--');
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {EmojiTypes} from '@mm-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import {handleSuccessfulLogin} from 'app/actions/views/login';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
receivedPostsSince,
|
||||
receivedPostsInThread,
|
||||
} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {Posts} from '@mm-redux/constants';
|
||||
import {getPost as selectPost, getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
@@ -28,28 +28,6 @@ import {getChannelSinceValue} from '@utils/channels';
|
||||
|
||||
import {getEmojisInPosts} from './emoji';
|
||||
|
||||
export function sendEphemeralPost(message, channelId = '', parentId = '', userId = '0') {
|
||||
return async (dispatch, getState) => {
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
id: generateId(),
|
||||
user_id: userId,
|
||||
channel_id: channelId || getCurrentChannelId(getState()),
|
||||
message,
|
||||
type: Posts.POST_TYPES.EPHEMERAL,
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: parentId,
|
||||
parent_id: parentId,
|
||||
props: {},
|
||||
};
|
||||
|
||||
dispatch(receivedNewPost(post));
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
||||
return async (dispatch) => {
|
||||
const timestamp = Date.now();
|
||||
@@ -73,14 +51,13 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
};
|
||||
}
|
||||
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options, getDynamicOptions) {
|
||||
export function setAutocompleteSelector(dataSource, onSelect, options) {
|
||||
return {
|
||||
type: ViewTypes.SELECTED_ACTION_MENU,
|
||||
data: {
|
||||
dataSource,
|
||||
onSelect,
|
||||
options,
|
||||
getDynamicOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {PostTypes, UserTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {fetchMyChannelsAndMembers, getChannelAndMyMember} from '@mm-redux/action
|
||||
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';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {GeneralTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import * as HelperActions from '@mm-redux/actions/helpers';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
export function handleRefreshAppsBindings() {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
|
||||
const state = getState();
|
||||
if (appsEnabled(state)) {
|
||||
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import configureMockStore from 'redux-mock-store';
|
||||
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as TeamActions from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
|
||||
@@ -5,7 +5,7 @@ import {fetchChannelAndMyMember} from '@actions/helpers/channels';
|
||||
import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {markChannelAsRead} from '@mm-redux/actions/channels';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -7,7 +7,7 @@ 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) => {
|
||||
@@ -155,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({
|
||||
@@ -378,9 +377,6 @@ function handleEvent(msg: WebSocketMessage) {
|
||||
return dispatch(handleOpenDialogEvent(msg));
|
||||
case WebsocketEvents.RECEIVED_GROUP:
|
||||
return dispatch(handleGroupUpdatedEvent(msg));
|
||||
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS: {
|
||||
return dispatch(handleRefreshAppsBindings());
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import * as ChannelActions from '@mm-redux/actions/channels';
|
||||
import * as PostActions from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General, Posts} from '@mm-redux/constants';
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {RoleTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';
|
||||
|
||||
@@ -5,7 +5,7 @@ import assert from 'assert';
|
||||
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
|
||||
import * as Actions from '@actions/websocket';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {loadChannelsForTeam} from '@actions/views/channel';
|
||||
import {getMe} from '@actions/views/user';
|
||||
import {ChannelTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
|
||||
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getAllChannels, getCurrentChannelId, getChannelMembersInChannels} from '@mm-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -171,7 +171,7 @@ export function handleUserRoleUpdated(msg: WebSocketMessage) {
|
||||
|
||||
dispatch({
|
||||
type: RoleTypes.RECEIVED_ROLES,
|
||||
data,
|
||||
data: data.roles,
|
||||
});
|
||||
} catch {
|
||||
// do nothing
|
||||
|
||||
@@ -11,7 +11,7 @@ import configureMockStore from 'redux-mock-store';
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
export interface ClientAppsMix {
|
||||
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
|
||||
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
|
||||
}
|
||||
|
||||
const ClientApps = (superclass: any) => class extends superclass {
|
||||
executeAppCall = async (call: AppCallRequest, type: AppCallType) => {
|
||||
const callCopy = {
|
||||
...call,
|
||||
path: `${call.path}/${type}`,
|
||||
context: {
|
||||
...call.context,
|
||||
user_agent: 'mobile',
|
||||
},
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getAppsProxyRoute()}/api/v1/call`,
|
||||
{method: 'post', body: JSON.stringify(callCopy)},
|
||||
);
|
||||
}
|
||||
|
||||
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
|
||||
const params = {
|
||||
user_id: userID,
|
||||
channel_id: channelID,
|
||||
team_id: teamID,
|
||||
user_agent: 'mobile',
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientApps;
|
||||
@@ -1,373 +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}`;
|
||||
}
|
||||
|
||||
getReactionsRoute() {
|
||||
return `${this.getBaseRoute()}/reactions`;
|
||||
}
|
||||
|
||||
getCommandsRoute() {
|
||||
return `${this.getBaseRoute()}/commands`;
|
||||
}
|
||||
|
||||
getFilesRoute() {
|
||||
return `${this.getBaseRoute()}/files`;
|
||||
}
|
||||
|
||||
getFileRoute(fileId: string) {
|
||||
return `${this.getFilesRoute()}/${fileId}`;
|
||||
}
|
||||
|
||||
getPreferencesRoute(userId: string) {
|
||||
return `${this.getUserRoute(userId)}/preferences`;
|
||||
}
|
||||
|
||||
getIncomingHooksRoute() {
|
||||
return `${this.getBaseRoute()}/hooks/incoming`;
|
||||
}
|
||||
|
||||
getIncomingHookRoute(hookId: string) {
|
||||
return `${this.getBaseRoute()}/hooks/incoming/${hookId}`;
|
||||
}
|
||||
|
||||
getOutgoingHooksRoute() {
|
||||
return `${this.getBaseRoute()}/hooks/outgoing`;
|
||||
}
|
||||
|
||||
getOutgoingHookRoute(hookId: string) {
|
||||
return `${this.getBaseRoute()}/hooks/outgoing/${hookId}`;
|
||||
}
|
||||
|
||||
getOAuthRoute() {
|
||||
return `${this.url}/oauth`;
|
||||
}
|
||||
|
||||
getOAuthAppsRoute() {
|
||||
return `${this.getBaseRoute()}/oauth/apps`;
|
||||
}
|
||||
|
||||
getOAuthAppRoute(appId: string) {
|
||||
return `${this.getOAuthAppsRoute()}/${appId}`;
|
||||
}
|
||||
|
||||
getEmojisRoute() {
|
||||
return `${this.getBaseRoute()}/emoji`;
|
||||
}
|
||||
|
||||
getEmojiRoute(emojiId: string) {
|
||||
return `${this.getEmojisRoute()}/${emojiId}`;
|
||||
}
|
||||
|
||||
getBrandRoute() {
|
||||
return `${this.getBaseRoute()}/brand`;
|
||||
}
|
||||
|
||||
getBrandImageUrl(timestamp: string) {
|
||||
return `${this.getBrandRoute()}/image?t=${timestamp}`;
|
||||
}
|
||||
|
||||
getDataRetentionRoute() {
|
||||
return `${this.getBaseRoute()}/data_retention`;
|
||||
}
|
||||
|
||||
getRolesRoute() {
|
||||
return `${this.getBaseRoute()}/roles`;
|
||||
}
|
||||
|
||||
getTimezonesRoute() {
|
||||
return `${this.getBaseRoute()}/system/timezones`;
|
||||
}
|
||||
|
||||
getRedirectLocationRoute() {
|
||||
return `${this.getBaseRoute()}/redirect_location`;
|
||||
}
|
||||
|
||||
getBotsRoute() {
|
||||
return `${this.getBaseRoute()}/bots`;
|
||||
}
|
||||
|
||||
getBotRoute(botUserId: string) {
|
||||
return `${this.getBotsRoute()}/${botUserId}`;
|
||||
}
|
||||
|
||||
getAppsProxyRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
|
||||
const serverUrl = this.getUrl();
|
||||
const parsed = urlParse(url);
|
||||
const {redirects} = response.rnfbRespInfo;
|
||||
if (redirects) {
|
||||
const redirectUrl = urlParse(redirects[redirects.length - 1]);
|
||||
|
||||
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
|
||||
this.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
doFetch = async (url: string, options: Options) => {
|
||||
const {data} = await this.doFetchWithResponse(url, options);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
doFetchWithResponse = async (url: string, options: Options) => {
|
||||
const response = await fetch(url, this.getOptions(options));
|
||||
const headers = parseAndMergeNestedHeaders(response.headers);
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: 'Received invalid response from the server.',
|
||||
intl: {
|
||||
id: 'mobile.request.invalid_response',
|
||||
defaultMessage: 'Received invalid response from the server.',
|
||||
},
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
if (headers.has(ClientConstants.HEADER_X_VERSION_ID) && !headers.get('Cache-Control')) {
|
||||
const serverVersion = headers.get(ClientConstants.HEADER_X_VERSION_ID);
|
||||
if (serverVersion && this.serverVersion !== serverVersion) {
|
||||
this.serverVersion = serverVersion;
|
||||
}
|
||||
}
|
||||
|
||||
if (headers.has(ClientConstants.HEADER_X_CLUSTER_ID)) {
|
||||
const clusterId = headers.get(ClientConstants.HEADER_X_CLUSTER_ID);
|
||||
if (clusterId && this.clusterId !== clusterId) {
|
||||
this.clusterId = clusterId;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
response,
|
||||
headers,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
const msg = data.message || '';
|
||||
|
||||
if (this.logToConsole) {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: msg,
|
||||
server_error_id: data.id,
|
||||
status_code: data.status_code,
|
||||
url,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function parseAndMergeNestedHeaders(originalHeaders: any) {
|
||||
const headers = new Map();
|
||||
let nestedHeaders = new Map();
|
||||
originalHeaders.forEach((val: string, key: string) => {
|
||||
const capitalizedKey = key.replace(/\b[a-z]/g, (l) => l.toUpperCase());
|
||||
let realVal = val;
|
||||
if (val && val.match(/\n\S+:\s\S+/)) {
|
||||
const nestedHeaderStrings = val.split('\n');
|
||||
realVal = nestedHeaderStrings.shift() as string;
|
||||
const moreNestedHeaders = new Map(
|
||||
nestedHeaderStrings.map((h: any) => h.split(/:\s/)),
|
||||
);
|
||||
nestedHeaders = new Map([...nestedHeaders, ...moreNestedHeaders]);
|
||||
}
|
||||
headers.set(capitalizedKey, realVal);
|
||||
});
|
||||
return new Map([...headers, ...nestedHeaders]);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import type {Bot} from '@mm-redux/types/bots';
|
||||
|
||||
export interface ClientBotsMix {
|
||||
getBot: (botUserId: string) => Promise<Bot>;
|
||||
getBots: (page?: number, perPage?: number) => Promise<Bot[]>;
|
||||
getBotsIncludeDeleted: (page?: number, perPage?: number) => Promise<Bot[]>;
|
||||
}
|
||||
|
||||
const PER_PAGE_DEFAULT = 60;
|
||||
|
||||
const ClientBots = (superclass: any) => class extends superclass {
|
||||
getBot = async (botUserId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotRoute(botUserId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientBots;
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Channel, ChannelMemberCountByGroup, ChannelMembership, ChannelNotifyProps, ChannelStats} from '@mm-redux/types/channels';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientChannelsMix {
|
||||
getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise<any>;
|
||||
createChannel: (channel: Channel) => Promise<Channel>;
|
||||
createDirectChannel: (userIds: string[]) => Promise<Channel>;
|
||||
createGroupChannel: (userIds: string[]) => Promise<Channel>;
|
||||
deleteChannel: (channelId: string) => Promise<any>;
|
||||
unarchiveChannel: (channelId: string) => Promise<Channel>;
|
||||
updateChannel: (channel: Channel) => Promise<Channel>;
|
||||
convertChannelToPrivate: (channelId: string) => Promise<Channel>;
|
||||
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
|
||||
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
|
||||
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => Promise<any>;
|
||||
getChannel: (channelId: string) => Promise<Channel>;
|
||||
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number) => Promise<Channel[]>;
|
||||
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
|
||||
getMyChannelMembers: (teamId: string) => Promise<ChannelMembership[]>;
|
||||
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
|
||||
getChannelTimezones: (channelId: string) => Promise<string[]>;
|
||||
getChannelMember: (channelId: string, userId: string) => Promise<ChannelMembership>;
|
||||
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
|
||||
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
|
||||
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
|
||||
getChannelStats: (channelId: string) => Promise<ChannelStats>;
|
||||
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
|
||||
viewMyChannel: (channelId: string, prevChannelId?: string) => Promise<any>;
|
||||
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
|
||||
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
|
||||
}
|
||||
|
||||
const ClientChannels = (superclass: any) => class extends superclass {
|
||||
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
|
||||
const queryData = {
|
||||
page,
|
||||
per_page: perPage,
|
||||
not_associated_to_group: notAssociatedToGroup,
|
||||
exclude_default_channels: excludeDefaultChannels,
|
||||
include_total_count: includeTotalCount,
|
||||
};
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}${buildQueryString(queryData)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
createChannel = async (channel: Channel) => {
|
||||
analytics.trackAPI('api_channels_create', {team_id: channel.team_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(channel)},
|
||||
);
|
||||
};
|
||||
|
||||
createDirectChannel = async (userIds: string[]) => {
|
||||
analytics.trackAPI('api_channels_create_direct');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/direct`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
createGroupChannel = async (userIds: string[]) => {
|
||||
analytics.trackAPI('api_channels_create_group');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/group`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_delete', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
unarchiveChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/restore`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannel = async (channel: Channel) => {
|
||||
analytics.trackAPI('api_channels_update', {channel_id: channel.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channel.id)}`,
|
||||
{method: 'put', body: JSON.stringify(channel)},
|
||||
);
|
||||
};
|
||||
|
||||
convertChannelToPrivate = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/convert`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelPrivacy = async (channelId: string, privacy: any) => {
|
||||
analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/privacy`,
|
||||
{method: 'put', body: JSON.stringify({privacy})},
|
||||
);
|
||||
};
|
||||
|
||||
patchChannel = async (channelId: string, channelPatch: Partial<Channel>) => {
|
||||
analytics.trackAPI('api_channels_patch', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(channelPatch)},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => {
|
||||
analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`,
|
||||
{method: 'put', body: JSON.stringify(props)},
|
||||
);
|
||||
};
|
||||
|
||||
getChannel = async (channelId: string) => {
|
||||
analytics.trackAPI('api_channel_get', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelByName = async (teamId: string, channelName: string, includeDeleted = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => {
|
||||
analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, lastDeleteAt = 0) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
|
||||
include_deleted: includeDeleted,
|
||||
last_delete_at: lastDeleteAt,
|
||||
})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannelMember = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, 'me')}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyChannelMembers = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels/members`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMembers = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelTimezones = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/timezones`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMember = async (channelId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMembersByIds = async (channelId: string, userIds: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}/ids`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
addToChannel = async (userId: string, channelId: string, postRootId = '') => {
|
||||
analytics.trackAPI('api_channels_add_member', {channel_id: channelId});
|
||||
|
||||
const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId};
|
||||
return this.doFetch(
|
||||
`${this.getChannelMembersRoute(channelId)}`,
|
||||
{method: 'post', body: JSON.stringify(member)},
|
||||
);
|
||||
};
|
||||
|
||||
removeFromChannel = async (userId: string, channelId: string) => {
|
||||
analytics.trackAPI('api_channels_remove_member', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, userId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelStats = async (channelId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/stats`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelMemberCountsByGroup = async (channelId: string, includeTimezones: boolean) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/member_counts_by_group?include_timezones=${includeTimezones}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
viewMyChannel = async (channelId: string, prevChannelId?: string) => {
|
||||
const data = {channel_id: channelId, prev_channel_id: prevChannelId};
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/members/me/view`,
|
||||
{method: 'post', body: JSON.stringify(data)},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteChannels = async (teamId: string, name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteChannelsForSearch = async (teamId: string, name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search_autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
searchChannels = async (teamId: string, term: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search`,
|
||||
{method: 'post', body: JSON.stringify({term})},
|
||||
);
|
||||
};
|
||||
|
||||
searchArchivedChannels = async (teamId: string, term: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/channels/search_archived`,
|
||||
{method: 'post', body: JSON.stringify({term})},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientChannels;
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const HEADER_AUTH = 'Authorization';
|
||||
export const HEADER_BEARER = 'BEARER';
|
||||
export const HEADER_REQUESTED_WITH = 'X-Requested-With';
|
||||
export const HEADER_USER_AGENT = 'User-Agent';
|
||||
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
|
||||
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
|
||||
export const HEADER_TOKEN = 'Token';
|
||||
export const HEADER_X_VERSION_ID = 'X-Version-Id';
|
||||
export const DEFAULT_LIMIT_BEFORE = 30;
|
||||
export const DEFAULT_LIMIT_AFTER = 30;
|
||||
export const PER_PAGE_DEFAULT = 60;
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import FormData from 'form-data';
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {CustomEmoji} from '@mm-redux/types/emojis';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientEmojisMix {
|
||||
createCustomEmoji: (emoji: CustomEmoji, imageData: any) => Promise<CustomEmoji>;
|
||||
getCustomEmoji: (id: string) => Promise<CustomEmoji>;
|
||||
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
|
||||
getCustomEmojis: (page?: number, perPage?: number, sort?: string) => Promise<CustomEmoji[]>;
|
||||
deleteCustomEmoji: (emojiId: string) => Promise<any>;
|
||||
getSystemEmojiImageUrl: (filename: string) => string;
|
||||
getCustomEmojiImageUrl: (id: string) => string;
|
||||
searchCustomEmoji: (term: string, options?: Record<string, any>) => Promise<CustomEmoji[]>;
|
||||
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
|
||||
}
|
||||
|
||||
const ClientEmojis = (superclass: any) => class extends superclass {
|
||||
createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => {
|
||||
analytics.trackAPI('api_emoji_custom_add');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', imageData);
|
||||
formData.append('emoji', JSON.stringify(emoji));
|
||||
const request: any = {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
};
|
||||
|
||||
if (formData.getBoundary) {
|
||||
request.headers = {
|
||||
'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}`,
|
||||
request,
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmoji = async (id: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/${id}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmojiByName = async (name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/name/${name}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCustomEmojis = async (page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}${buildQueryString({page, per_page: perPage, sort})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
deleteCustomEmoji = async (emojiId: string) => {
|
||||
analytics.trackAPI('api_emoji_custom_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getEmojiRoute(emojiId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getSystemEmojiImageUrl = (filename: string) => {
|
||||
return `${this.url}/static/emoji/${filename}.png`;
|
||||
};
|
||||
|
||||
getCustomEmojiImageUrl = (id: string) => {
|
||||
return `${this.getEmojiRoute(id)}/image`;
|
||||
};
|
||||
|
||||
searchCustomEmoji = async (term: string, options = {}) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/search`,
|
||||
{method: 'post', body: JSON.stringify({term, ...options})},
|
||||
);
|
||||
};
|
||||
|
||||
autocompleteCustomEmoji = async (name: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/autocomplete${buildQueryString({name})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientEmojis;
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {cleanUrlForLogging} from '@mm-redux/utils/sentry';
|
||||
|
||||
export default class ClientError extends Error {
|
||||
url: string;
|
||||
intl: { defaultMessage: string; id: string } | { defaultMessage: string; id: string } | { id: string; defaultMessage: string; values: any } | { id: string; defaultMessage: string };
|
||||
server_error_id: any;
|
||||
status_code: any;
|
||||
details: Error;
|
||||
constructor(baseUrl: string, data: any) {
|
||||
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
|
||||
|
||||
this.message = data.message;
|
||||
this.url = data.url;
|
||||
this.intl = data.intl;
|
||||
this.server_error_id = data.server_error_id;
|
||||
this.status_code = data.status_code;
|
||||
this.details = data.details;
|
||||
|
||||
// Ensure message is treated as a property of this class when object spreading. Without this,
|
||||
// copying the object by using `{...error}` would not include the message.
|
||||
Object.defineProperty(this, 'message', {enumerable: true});
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientFilesMix {
|
||||
getFileUrl: (fileId: string, timestamp: number) => string;
|
||||
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePreviewUrl: (fileId: string, timestamp: number) => string;
|
||||
getFilePublicLink: (fileId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientFiles = (superclass: any) => class extends superclass {
|
||||
getFileUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFileThumbnailUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}/thumbnail`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFilePreviewUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.getFileRoute(fileId)}/preview`;
|
||||
if (timestamp) {
|
||||
url += `?${timestamp}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
getFilePublicLink = async (fileId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getFileRoute(fileId)}/link`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientFiles;
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Config} from '@mm-redux/types/config';
|
||||
import {Role} from '@mm-redux/types/roles';
|
||||
import {Dictionary} from '@mm-redux/types/utilities';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import ClientError from './error';
|
||||
|
||||
export interface ClientGeneralMix {
|
||||
getOpenGraphMetadata: (url: string) => Promise<any>;
|
||||
ping: () => Promise<any>;
|
||||
logClientError: (message: string, level?: string) => Promise<any>;
|
||||
getClientConfigOld: () => Promise<Config>;
|
||||
getClientLicenseOld: () => Promise<any>;
|
||||
getTimezones: () => Promise<string[]>;
|
||||
getDataRetentionPolicy: () => Promise<any>;
|
||||
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
|
||||
getRedirectLocation: (urlParam: string) => Promise<Dictionary<string>>;
|
||||
}
|
||||
|
||||
const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
getOpenGraphMetadata = async (url: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/opengraph`,
|
||||
{method: 'post', body: JSON.stringify({url})},
|
||||
);
|
||||
};
|
||||
|
||||
ping = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/system/ping?time=${Date.now()}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
logClientError = async (message: string, level = 'ERROR') => {
|
||||
const url = `${this.getBaseRoute()}/logs`;
|
||||
|
||||
if (!this.enableLogging) {
|
||||
throw new ClientError(this.getUrl(), {
|
||||
message: 'Logging disabled.',
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
url,
|
||||
{method: 'post', body: JSON.stringify({message, level})},
|
||||
);
|
||||
};
|
||||
|
||||
getClientConfigOld = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/config/client?format=old`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getClientLicenseOld = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/license/client?format=old`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTimezones = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getTimezonesRoute()}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getDataRetentionPolicy = () => {
|
||||
return this.doFetch(
|
||||
`${this.getDataRetentionRoute()}/policy`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getRolesByNames = async (rolesNames: string[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getRolesRoute()}/names`,
|
||||
{method: 'post', body: JSON.stringify(rolesNames)},
|
||||
);
|
||||
};
|
||||
|
||||
getRedirectLocation = async (urlParam: string) => {
|
||||
if (!urlParam.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const url = `${this.getRedirectLocationRoute()}${buildQueryString({url: urlParam})}`;
|
||||
return this.doFetch(url, {method: 'get'});
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientGeneral;
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Group} from '@mm-redux/types/groups';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientGroupsMix {
|
||||
getGroups: (filterAllowReference?: boolean, page?: number, perPage?: number) => Promise<Group[]>;
|
||||
getGroupsByUserId: (userID: string) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannelsInTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
}
|
||||
|
||||
const ClientGroups = (superclass: any) => class extends superclass {
|
||||
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT, since = 0) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getGroupsByUserId = async (userID: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/${userID}/groups`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToChannelsInTeam = async (teamID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToChannel = async (channelID: string, filterAllowReference = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientGroups;
|
||||
@@ -1,56 +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 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,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
ClientApps,
|
||||
ClientBots,
|
||||
ClientChannels,
|
||||
ClientEmojis,
|
||||
ClientFiles,
|
||||
ClientGeneral,
|
||||
ClientGroups,
|
||||
ClientIntegrations,
|
||||
ClientPosts,
|
||||
ClientPreferences,
|
||||
ClientTeams,
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
||||
export {Client4, Client, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID};
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Command, DialogSubmission} from '@mm-redux/types/integrations';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientIntegrationsMix {
|
||||
getCommandsList: (teamId: string) => Promise<Command[]>;
|
||||
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, commandArgs?: Record<string, any>) => Promise<Command[]>;
|
||||
getAutocompleteCommandsList: (teamId: string, page?: number, perPage?: number) => Promise<Command[]>;
|
||||
executeCommand: (command: Command, commandArgs?: Record<string, any>) => Promise<any>;
|
||||
addCommand: (command: Command) => Promise<Command>;
|
||||
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
getCommandsList = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}?team_id=${teamId}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getCommandAutocompleteSuggestionsList = async (userInput: string, teamId: string, commandArgs: {}) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({...commandArgs, user_input: userInput})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getAutocompleteCommandsList = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/commands/autocomplete${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
executeCommand = async (command: Command, commandArgs = {}) => {
|
||||
analytics.trackAPI('api_integrations_used');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}/execute`,
|
||||
{method: 'post', body: JSON.stringify({command, ...commandArgs})},
|
||||
);
|
||||
};
|
||||
|
||||
addCommand = async (command: Command) => {
|
||||
analytics.trackAPI('api_integrations_created');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(command)},
|
||||
);
|
||||
};
|
||||
|
||||
submitInteractiveDialog = async (data: DialogSubmission) => {
|
||||
analytics.trackAPI('api_interactive_messages_dialog_submitted');
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/actions/dialogs/submit`,
|
||||
{method: 'post', body: JSON.stringify(data)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientIntegrations;
|
||||
@@ -1,237 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {FileInfo} from '@mm-redux/types/files';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientPostsMix {
|
||||
createPost: (post: Post) => Promise<Post>;
|
||||
updatePost: (post: Post) => Promise<Post>;
|
||||
getPost: (postId: string) => Promise<Post>;
|
||||
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
|
||||
deletePost: (postId: string) => Promise<any>;
|
||||
getPostThread: (postId: string) => Promise<any>;
|
||||
getPosts: (channelId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPostsSince: (channelId: string, since: number) => Promise<any>;
|
||||
getPostsBefore: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
|
||||
getFlaggedPosts: (userId: string, channelId?: string, teamId?: string, page?: number, perPage?: number) => Promise<any>;
|
||||
getPinnedPosts: (channelId: string) => Promise<any>;
|
||||
markPostAsUnread: (userId: string, postId: string) => Promise<any>;
|
||||
pinPost: (postId: string) => Promise<any>;
|
||||
unpinPost: (postId: string) => Promise<any>;
|
||||
addReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
|
||||
removeReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
|
||||
getReactionsForPost: (postId: string) => Promise<any>;
|
||||
searchPostsWithParams: (teamId: string, params: any) => Promise<any>;
|
||||
searchPosts: (teamId: string, terms: string, isOrSearch: boolean) => Promise<any>;
|
||||
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientPosts = (superclass: any) => class extends superclass {
|
||||
createPost = async (post: Post) => {
|
||||
analytics.trackAPI('api_posts_create', {channel_id: post.channel_id});
|
||||
|
||||
if (post.root_id != null && post.root_id !== '') {
|
||||
analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id});
|
||||
}
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(post)},
|
||||
);
|
||||
};
|
||||
|
||||
updatePost = async (post: Post) => {
|
||||
analytics.trackAPI('api_posts_update', {channel_id: post.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(post.id)}`,
|
||||
{method: 'put', body: JSON.stringify(post)},
|
||||
);
|
||||
};
|
||||
|
||||
getPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
patchPost = async (postPatch: Partial<Post> & {id: string}) => {
|
||||
analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postPatch.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(postPatch)},
|
||||
);
|
||||
};
|
||||
|
||||
deletePost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostThread = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/thread`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsSince = async (channelId: string, since: number) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({since})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_before', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_after', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getFileInfosForPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/files/info`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getFlaggedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
analytics.trackAPI('api_posts_get_flagged', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getPinnedPosts = async (channelId: string) => {
|
||||
analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/pinned`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
markPostAsUnread = async (userId: string, postId: string) => {
|
||||
analytics.trackAPI('api_post_set_unread_post');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/set_unread`,
|
||||
{method: 'post'},
|
||||
);
|
||||
}
|
||||
|
||||
pinPost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_pin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/pin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
unpinPost = async (postId: string) => {
|
||||
analytics.trackAPI('api_posts_unpin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/unpin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
addReaction = async (userId: string, postId: string, emojiName: string) => {
|
||||
analytics.trackAPI('api_reactions_save', {post_id: postId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getReactionsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify({user_id: userId, post_id: postId, emoji_name: emojiName})},
|
||||
);
|
||||
};
|
||||
|
||||
removeReaction = async (userId: string, postId: string, emojiName: string) => {
|
||||
analytics.trackAPI('api_reactions_delete', {post_id: postId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getReactionsForPost = async (postId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/reactions`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
searchPostsWithParams = async (teamId: string, params: any) => {
|
||||
analytics.trackAPI('api_posts_search', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/posts/search`,
|
||||
{method: 'post', body: JSON.stringify(params)},
|
||||
);
|
||||
};
|
||||
|
||||
searchPosts = async (teamId: string, terms: string, isOrSearch: boolean) => {
|
||||
return this.searchPostsWithParams(teamId, {terms, is_or_search: isOrSearch});
|
||||
};
|
||||
|
||||
doPostAction = async (postId: string, actionId: string, selectedOption = '') => {
|
||||
return this.doPostActionWithCookie(postId, actionId, '', selectedOption);
|
||||
};
|
||||
|
||||
doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
|
||||
if (selectedOption) {
|
||||
analytics.trackAPI('api_interactive_messages_menu_selected');
|
||||
} else {
|
||||
analytics.trackAPI('api_interactive_messages_button_clicked');
|
||||
}
|
||||
|
||||
const msg: any = {
|
||||
selected_option: selectedOption,
|
||||
};
|
||||
if (actionCookie !== '') {
|
||||
msg.cookie = actionCookie;
|
||||
}
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/actions/${encodeURIComponent(actionId)}`,
|
||||
{method: 'post', body: JSON.stringify(msg)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPosts;
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {PreferenceType} from '@mm-redux/types/preferences';
|
||||
|
||||
export interface ClientPreferencesMix {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
|
||||
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
|
||||
getMyPreferences: () => Promise<PreferenceType>;
|
||||
}
|
||||
|
||||
const ClientPreferences = (superclass: any) => class extends superclass {
|
||||
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute(userId)}`,
|
||||
{method: 'put', body: JSON.stringify(preferences)},
|
||||
);
|
||||
};
|
||||
|
||||
getMyPreferences = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute('me')}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute(userId)}/delete`,
|
||||
{method: 'post', body: JSON.stringify(preferences)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientPreferences;
|
||||
@@ -1,177 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Team, TeamMembership, TeamUnread} from '@mm-redux/types/teams';
|
||||
import {buildQueryString} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientTeamsMix {
|
||||
createTeam: (team: Team) => Promise<Team>;
|
||||
deleteTeam: (teamId: string) => Promise<any>;
|
||||
updateTeam: (team: Team) => Promise<Team>;
|
||||
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<any>;
|
||||
getTeam: (teamId: string) => Promise<Team>;
|
||||
getTeamByName: (teamName: string) => Promise<Team>;
|
||||
getMyTeams: () => Promise<Team[]>;
|
||||
getTeamsForUser: (userId: string) => Promise<Team[]>;
|
||||
getMyTeamMembers: () => Promise<TeamMembership[]>;
|
||||
getMyTeamUnreads: () => Promise<TeamUnread[]>;
|
||||
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
|
||||
getTeamMember: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
joinTeam: (inviteId: string) => Promise<TeamMembership>;
|
||||
removeFromTeam: (teamId: string, userId: string) => Promise<any>;
|
||||
getTeamStats: (teamId: string) => Promise<any>;
|
||||
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
|
||||
}
|
||||
|
||||
const ClientTeams = (superclass: any) => class extends superclass {
|
||||
createTeam = async (team: Team) => {
|
||||
analytics.trackAPI('api_teams_create');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteTeam = async (teamId: string) => {
|
||||
analytics.trackAPI('api_teams_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
updateTeam = async (team: Team) => {
|
||||
analytics.trackAPI('api_teams_update_name', {team_id: team.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(team.id)}`,
|
||||
{method: 'put', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
patchTeam = async (team: Partial<Team> & {id: string}) => {
|
||||
analytics.trackAPI('api_teams_patch_name', {team_id: team.id});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(team.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeam = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
this.getTeamRoute(teamId),
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamByName = async (teamName: string) => {
|
||||
analytics.trackAPI('api_teams_get_team_by_name');
|
||||
|
||||
return this.doFetch(
|
||||
this.getTeamNameRoute(teamName),
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeams = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamsForUser = async (userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/teams`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeamMembers = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/members`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getMyTeamUnreads = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/unread`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamMembers = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamMember = async (teamId: string, userId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMemberRoute(teamId, userId)}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
addToTeam = async (teamId: string, userId: string) => {
|
||||
analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
|
||||
|
||||
const member = {user_id: userId, team_id: teamId};
|
||||
return this.doFetch(
|
||||
`${this.getTeamMembersRoute(teamId)}`,
|
||||
{method: 'post', body: JSON.stringify(member)},
|
||||
);
|
||||
};
|
||||
|
||||
joinTeam = async (inviteId: string) => {
|
||||
const query = buildQueryString({invite_id: inviteId});
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}/members/invite${query}`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
removeFromTeam = async (teamId: string, userId: string) => {
|
||||
analytics.trackAPI('api_teams_remove_members', {team_id: teamId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamMemberRoute(teamId, userId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamStats = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}/stats`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamIconUrl = (teamId: string, lastTeamIconUpdate: number) => {
|
||||
const params: any = {};
|
||||
if (lastTeamIconUpdate) {
|
||||
params._ = lastTeamIconUpdate;
|
||||
}
|
||||
|
||||
return `${this.getTeamRoute(teamId)}/image${buildQueryString(params)}`;
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientTeams;
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export interface ClientTosMix {
|
||||
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<any>;
|
||||
getTermsOfService: () => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientTos = (superclass: any) => class extends superclass {
|
||||
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/terms_of_service`,
|
||||
{method: 'post', body: JSON.stringify({termsOfServiceId, accepted})},
|
||||
);
|
||||
}
|
||||
|
||||
getTermsOfService = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/terms_of_service`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ClientTos;
|
||||
@@ -1,398 +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 {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>;
|
||||
}
|
||||
|
||||
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)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientUsers;
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
exports[`AtMention should match snapshot, no highlight 1`] = `
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={Array []}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
@John.Smith
|
||||
</Text>
|
||||
@@ -20,11 +22,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
@@ -33,8 +31,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
|
||||
"color": "#ff0000",
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "#ffe577",
|
||||
"color": "#166de0",
|
||||
"backgroundColor": "yellow",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -48,11 +45,7 @@ exports[`AtMention should match snapshot, without highlight 1`] = `
|
||||
<Text
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "yellow",
|
||||
}
|
||||
}
|
||||
style={Object {}}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
|
||||
@@ -18,9 +19,9 @@ export default class AtMention extends React.PureComponent {
|
||||
isSearchResult: PropTypes.bool,
|
||||
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.isRequired,
|
||||
@@ -138,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 = [];
|
||||
|
||||
@@ -154,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) {
|
||||
@@ -174,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;
|
||||
}
|
||||
@@ -192,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}
|
||||
@@ -205,7 +207,7 @@ export default class AtMention extends React.PureComponent {
|
||||
}
|
||||
|
||||
if (highlighted) {
|
||||
mentionTextStyle.push({backgroundColor, color: theme.mentionHighlightLink});
|
||||
mentionTextStyle.push({backgroundColor});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
|
||||
import AtMention from './at_mention.js';
|
||||
|
||||
describe('AtMention', () => {
|
||||
@@ -16,7 +13,7 @@ describe('AtMention', () => {
|
||||
mentionName: 'John.Smith',
|
||||
mentionStyle: {color: '#ff0000'},
|
||||
textStyle: {backgroundColor: 'yellow'},
|
||||
theme: Preferences.THEMES.default,
|
||||
theme: {},
|
||||
};
|
||||
|
||||
test('should match snapshot, no highlight', () => {
|
||||
|
||||
@@ -3,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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -115,7 +115,6 @@ const AtMentionItem = (props: AtMentionItemProps) => {
|
||||
size={24}
|
||||
status={null}
|
||||
showStatus={false}
|
||||
testID={`${testID}.profile_picture`}
|
||||
/>
|
||||
</View>
|
||||
<BotTag
|
||||
|
||||
@@ -17,6 +17,7 @@ import {t} from 'app/utils/i18n';
|
||||
export default class ChannelMention extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
searchChannels: PropTypes.func.isRequired,
|
||||
autocompleteChannelsForSearch: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
@@ -205,7 +206,6 @@ export default class ChannelMention extends PureComponent {
|
||||
<ChannelMentionItem
|
||||
channelId={item}
|
||||
onPress={this.completeMention}
|
||||
testID={`autocomplete.channel_mention.item.${item}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -224,16 +224,15 @@ export default class ChannelMention extends PureComponent {
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
testID='channel_mention_suggestion.list'
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyExtractor={this.keyExtractor}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
style={[style.listView, {maxHeight: maxListHeight}]}
|
||||
sections={sections}
|
||||
testID='channel_mention_suggestion.list'
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
initialNumToRender={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
|
||||
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -56,6 +56,7 @@ function mapStateToProps(state, ownProps) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
searchChannels,
|
||||
autocompleteChannelsForSearch,
|
||||
}, dispatch),
|
||||
};
|
||||
|
||||
@@ -47,7 +47,6 @@ const ChannelMentionItem = (props) => {
|
||||
isGuest,
|
||||
name,
|
||||
onPress,
|
||||
testID,
|
||||
theme,
|
||||
type,
|
||||
} = props;
|
||||
@@ -78,7 +77,6 @@ const ChannelMentionItem = (props) => {
|
||||
key={channelId}
|
||||
onPress={completeMention}
|
||||
style={[style.row, margins]}
|
||||
testID={testID}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Text style={style.rowDisplayName}>{'@' + displayName}</Text>
|
||||
@@ -99,7 +97,6 @@ const ChannelMentionItem = (props) => {
|
||||
onPress={completeMention}
|
||||
style={margins}
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
testID={testID}
|
||||
type={'native'}
|
||||
>
|
||||
<View style={style.row}>
|
||||
|
||||
@@ -200,7 +200,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"bust_in_silhouette",
|
||||
"busts_in_silhouette",
|
||||
"butterfly",
|
||||
"ca",
|
||||
"cactus",
|
||||
"cake",
|
||||
"calendar",
|
||||
@@ -1024,7 +1023,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"pisces",
|
||||
"pitcairn_islands",
|
||||
"pizza",
|
||||
"pk",
|
||||
"place_of_worship",
|
||||
"plate_with_cutlery",
|
||||
"play_or_pause_button",
|
||||
@@ -1507,7 +1505,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"yen",
|
||||
"yin_yang",
|
||||
"yum",
|
||||
"za",
|
||||
"zambia",
|
||||
"zap",
|
||||
"zero",
|
||||
@@ -1715,7 +1712,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"bust_in_silhouette",
|
||||
"busts_in_silhouette",
|
||||
"butterfly",
|
||||
"ca",
|
||||
"cactus",
|
||||
"cake",
|
||||
"calendar",
|
||||
@@ -2539,7 +2535,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"pisces",
|
||||
"pitcairn_islands",
|
||||
"pizza",
|
||||
"pk",
|
||||
"place_of_worship",
|
||||
"plate_with_cutlery",
|
||||
"play_or_pause_button",
|
||||
@@ -3022,7 +3017,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
"yen",
|
||||
"yin_yang",
|
||||
"yum",
|
||||
"za",
|
||||
"zambia",
|
||||
"zap",
|
||||
"zero",
|
||||
@@ -3042,7 +3036,7 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
numColumns={1}
|
||||
onEndReachedThreshold={2}
|
||||
pageSize={10}
|
||||
removeClippedSubviews={true}
|
||||
removeClippedSubviews={false}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
style={
|
||||
|
||||
@@ -230,7 +230,6 @@ export default class EmojiSuggestion extends PureComponent {
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
|
||||
<FlatList
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"Complete": "thetrigger",
|
||||
"Description": "The Description",
|
||||
"Hint": "The Hint",
|
||||
"IconData": "iconurl.com",
|
||||
"Suggestion": "/thetrigger",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
extraData={
|
||||
Object {
|
||||
"active": true,
|
||||
"dataSource": Array [
|
||||
Object {
|
||||
"Complete": "thetrigger",
|
||||
"Description": "The Description",
|
||||
"Hint": "The Hint",
|
||||
"IconData": "iconurl.com",
|
||||
"Suggestion": "/thetrigger",
|
||||
},
|
||||
],
|
||||
"lastCommandRequest": 1234,
|
||||
}
|
||||
}
|
||||
horizontal={false}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={10}
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={50}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderRadius": 4,
|
||||
"flex": 1,
|
||||
"paddingTop": 8,
|
||||
},
|
||||
Object {
|
||||
"maxHeight": 50,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="slash_suggestion.list"
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
/>
|
||||
`;
|
||||
@@ -1,953 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {
|
||||
thunk,
|
||||
configureStore,
|
||||
Client4,
|
||||
AppBinding,
|
||||
checkForExecuteSuggestion,
|
||||
} from './tests/app_command_parser_test_dependencies';
|
||||
|
||||
import {
|
||||
AppCallResponseTypes,
|
||||
AppCallTypes,
|
||||
AutocompleteSuggestion,
|
||||
} from './app_command_parser_dependencies';
|
||||
|
||||
import {
|
||||
AppCommandParser,
|
||||
ParseState,
|
||||
ParsedCommand,
|
||||
} from './app_command_parser';
|
||||
|
||||
import {
|
||||
reduxTestState,
|
||||
testBindings,
|
||||
} from './tests/app_command_parser_test_data';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('AppCommandParser', () => {
|
||||
const makeStore = async (bindings: AppBinding[]) => {
|
||||
const initialState = {
|
||||
...reduxTestState,
|
||||
entities: {
|
||||
...reduxTestState.entities,
|
||||
apps: {bindings},
|
||||
},
|
||||
} as any;
|
||||
const testStore = await mockStore(initialState);
|
||||
|
||||
return testStore;
|
||||
};
|
||||
|
||||
const intl = {
|
||||
formatMessage: (message: {id: string, defaultMessage: string}) => {
|
||||
return message.defaultMessage;
|
||||
},
|
||||
};
|
||||
|
||||
let parser: AppCommandParser;
|
||||
beforeEach(async () => {
|
||||
const store = await makeStore(testBindings);
|
||||
parser = new AppCommandParser(store as any, intl, 'current_channel_id', 'root_id');
|
||||
});
|
||||
|
||||
type Variant = {
|
||||
expectError?: string;
|
||||
verify?(parsed: ParsedCommand): void;
|
||||
}
|
||||
|
||||
type TC = {
|
||||
title: string;
|
||||
command: string;
|
||||
submit: Variant;
|
||||
autocomplete?: Variant; // if undefined, use same checks as submnit
|
||||
}
|
||||
|
||||
const checkResult = (parsed: ParsedCommand, v: Variant) => {
|
||||
if (v.expectError) {
|
||||
expect(parsed.state).toBe(ParseState.Error);
|
||||
expect(parsed.error).toBe(v.expectError);
|
||||
} else {
|
||||
// expect(parsed).toBe(1);
|
||||
expect(parsed.error).toBe('');
|
||||
expect(v.verify).toBeTruthy();
|
||||
if (v.verify) {
|
||||
v.verify(parsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getSuggestionsBase', () => {
|
||||
test('string matches 1', () => {
|
||||
const res = parser.getSuggestionsBase('/');
|
||||
expect(res).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('string matches 2', () => {
|
||||
const res = parser.getSuggestionsBase('/ji');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string matches 3', () => {
|
||||
const res = parser.getSuggestionsBase('/jira');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string matches case insensitive', () => {
|
||||
const res = parser.getSuggestionsBase('/JiRa');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string is past base command', () => {
|
||||
const res = parser.getSuggestionsBase('/jira ');
|
||||
expect(res).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('other command matches', () => {
|
||||
const res = parser.getSuggestionsBase('/other');
|
||||
expect(res).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('string does not match', () => {
|
||||
const res = parser.getSuggestionsBase('/wrong');
|
||||
expect(res).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchBinding', () => {
|
||||
const table: TC[] = [
|
||||
{
|
||||
title: 'full command',
|
||||
command: '/jira issue create --project P --summary = "SUM MA RY" --verbose --epic=epic2',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('--project');
|
||||
expect(parsed.incompleteStart).toBe(19);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'full command case insensitive',
|
||||
command: '/JiRa IsSuE CrEaTe --PrOjEcT P --SuMmArY = "SUM MA RY" --VeRbOsE --EpIc=epic2',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('--PrOjEcT');
|
||||
expect(parsed.incompleteStart).toBe(19);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'incomplete top command',
|
||||
command: '/jir',
|
||||
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
submit: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
},
|
||||
{
|
||||
title: 'no space after the top command',
|
||||
command: '/jira',
|
||||
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'space after the top command',
|
||||
command: '/jira ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'middle of subcommand',
|
||||
command: '/jira iss',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('iss');
|
||||
expect(parsed.incompleteStart).toBe(9);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('iss');
|
||||
expect(parsed.incompleteStart).toBe(9);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'second subcommand, no space',
|
||||
command: '/jira issue',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('jira');
|
||||
expect(parsed.incomplete).toBe('issue');
|
||||
expect(parsed.incompleteStart).toBe(6);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('issue');
|
||||
expect(parsed.location).toBe('/jira/issue');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'token after the end of bindings, no space',
|
||||
command: '/jira issue create something',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.Command);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'token after the end of bindings, with space',
|
||||
command: '/jira issue create something ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndCommand);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.incomplete).toBe('something');
|
||||
expect(parsed.incompleteStart).toBe(20);
|
||||
}},
|
||||
},
|
||||
];
|
||||
|
||||
table.forEach((tc) => {
|
||||
test(tc.title, async () => {
|
||||
const bindings = testBindings[0].bindings as AppBinding[];
|
||||
|
||||
let a = new ParsedCommand(tc.command, parser, intl);
|
||||
a = await a.matchBinding(bindings, true);
|
||||
checkResult(a, tc.autocomplete || tc.submit);
|
||||
|
||||
let s = new ParsedCommand(tc.command, parser, intl);
|
||||
s = await s.matchBinding(bindings, false);
|
||||
checkResult(s, tc.submit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseForm', () => {
|
||||
const table: TC[] = [
|
||||
{
|
||||
title: 'happy full create',
|
||||
command: '/jira issue create --project `P 1` --summary "SUM MA RY" --verbose --epic=epic2',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy full create case insensitive',
|
||||
command: '/JiRa IsSuE CrEaTe --PrOjEcT `P 1` --SuMmArY "SUM MA RY" --VeRbOsE --EpIc=epic2',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('epic2');
|
||||
expect(parsed.incompleteStart).toBe(75);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.epic).toBe('epic2');
|
||||
expect(parsed.values?.summary).toBe('SUM MA RY');
|
||||
expect(parsed.values?.verbose).toBe('true');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'partial epic',
|
||||
command: '/jira issue create --project KT --summary "great feature" --epic M',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('M');
|
||||
expect(parsed.incompleteStart).toBe(65);
|
||||
expect(parsed.values?.project).toBe('KT');
|
||||
expect(parsed.values?.epic).toBeUndefined();
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.values?.epic).toBe('M');
|
||||
}},
|
||||
},
|
||||
|
||||
{
|
||||
title: 'happy full view',
|
||||
command: '/jira issue view --project=`P 1` MM-123',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('MM-123');
|
||||
expect(parsed.incompleteStart).toBe(33);
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe('MM-123');
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy view no parameters',
|
||||
command: '/jira issue view ',
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.StartParameter);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.incompleteStart).toBe(17);
|
||||
expect(parsed.values).toEqual({});
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'happy create flag no value',
|
||||
command: '/jira issue create --summary ',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.FlagValueSeparator);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({});
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndValue);
|
||||
expect(parsed.binding?.label).toBe('create');
|
||||
expect(parsed.form?.call?.path).toBe('/create-issue');
|
||||
expect(parsed.incomplete).toBe('');
|
||||
expect(parsed.values).toEqual({
|
||||
summary: '',
|
||||
});
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'error: unmatched tick',
|
||||
command: '/jira issue view --project `P 1',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.TickValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {expectError: 'Matching tick quote expected before end of input.'},
|
||||
},
|
||||
{
|
||||
title: 'error: unmatched quote',
|
||||
command: '/jira issue view --project "P \\1',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.QuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {expectError: 'Matching double quote expected before end of input.'},
|
||||
},
|
||||
{
|
||||
title: 'missing required fields not a problem for parseCommand',
|
||||
command: '/jira issue view --project "P 1"',
|
||||
autocomplete: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.incomplete).toBe('P 1');
|
||||
expect(parsed.incompleteStart).toBe(27);
|
||||
expect(parsed.values?.project).toBe(undefined);
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
submit: {verify: (parsed: ParsedCommand): void => {
|
||||
expect(parsed.state).toBe(ParseState.EndQuotedValue);
|
||||
expect(parsed.binding?.label).toBe('view');
|
||||
expect(parsed.form?.call?.path).toBe('/view-issue');
|
||||
expect(parsed.values?.project).toBe('P 1');
|
||||
expect(parsed.values?.issue).toBe(undefined);
|
||||
}},
|
||||
},
|
||||
{
|
||||
title: 'error: invalid flag',
|
||||
command: '/jira issue view --wrong test',
|
||||
submit: {expectError: 'Command does not accept flag `{flagName}`.'},
|
||||
},
|
||||
{
|
||||
title: 'error: unexpected positional',
|
||||
command: '/jira issue create wrong',
|
||||
submit: {expectError: 'Unable to identify argument.'},
|
||||
},
|
||||
{
|
||||
title: 'error: multiple equal signs',
|
||||
command: '/jira issue create --project == test',
|
||||
submit: {expectError: 'Multiple `=` signs are not allowed.'},
|
||||
},
|
||||
];
|
||||
|
||||
table.forEach((tc) => {
|
||||
test(tc.title, async () => {
|
||||
const bindings = testBindings[0].bindings as AppBinding[];
|
||||
|
||||
let a = new ParsedCommand(tc.command, parser, intl);
|
||||
a = await a.matchBinding(bindings, true);
|
||||
a = a.parseForm(true);
|
||||
checkResult(a, tc.autocomplete || tc.submit);
|
||||
|
||||
let s = new ParsedCommand(tc.command, parser, intl);
|
||||
s = await s.matchBinding(bindings, false);
|
||||
s = s.parseForm(false);
|
||||
checkResult(s, tc.submit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSuggestions', () => {
|
||||
test('subcommand 1', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'jira issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 1 case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'JiRa issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'jira issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'issue',
|
||||
Complete: 'JiRa issue',
|
||||
Hint: 'Issue hint',
|
||||
IconData: 'Issue icon',
|
||||
Description: 'Interact with Jira issues',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 with a space', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'view',
|
||||
Complete: 'jira issue view',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Description: 'View details of a Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'jira issue create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 2 with a space case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'view',
|
||||
Complete: 'JiRa IsSuE view',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Description: 'View details of a Jira issue',
|
||||
},
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'JiRa IsSuE create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 3 partial', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue c');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'jira issue create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('subcommand 3 partial case insensitive', async () => {
|
||||
const suggestions = await parser.getSuggestions('/JiRa IsSuE C');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Suggestion: 'create',
|
||||
Complete: 'JiRa IsSuE create',
|
||||
Hint: 'Create hint',
|
||||
IconData: 'Create icon',
|
||||
Description: 'Create a new Jira issue',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('view just after subcommand (positional)', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue view ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view',
|
||||
Description: 'The Jira issue key',
|
||||
Hint: '',
|
||||
IconData: '',
|
||||
Suggestion: 'issue: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('view flags just after subcommand', async () => {
|
||||
let suggestions = await parser.getSuggestions('/jira issue view -');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: '',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue view --');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue view --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: '',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flags just after subcommand', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create ');
|
||||
|
||||
let executeCommand: AutocompleteSuggestion[] = [];
|
||||
if (checkForExecuteSuggestion) {
|
||||
executeCommand = [
|
||||
{
|
||||
Complete: 'jira issue create _execute_current_command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
Hint: '',
|
||||
IconData: '_execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
...executeCommand,
|
||||
{
|
||||
Complete: 'jira issue create --project',
|
||||
Description: 'The Jira project description',
|
||||
Hint: 'The Jira project hint',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--project',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --verbose',
|
||||
Description: 'display details',
|
||||
Hint: 'yes or no!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--verbose',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--epic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('used flags do not appear', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT ');
|
||||
|
||||
let executeCommand: AutocompleteSuggestion[] = [];
|
||||
if (checkForExecuteSuggestion) {
|
||||
executeCommand = [
|
||||
{
|
||||
Complete: 'jira issue create --project KT _execute_current_command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
Hint: '',
|
||||
IconData: '_execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
...executeCommand,
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --verbose',
|
||||
Description: 'display details',
|
||||
Hint: 'yes or no!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--verbose',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--epic',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flags mid-flag', async () => {
|
||||
const mid = await parser.getSuggestions('/jira issue create --project KT --summ');
|
||||
expect(mid).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
]);
|
||||
|
||||
const full = await parser.getSuggestions('/jira issue create --project KT --summary');
|
||||
expect(full).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: '--summary',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('empty text value suggestion', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('partial text value suggestion', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary Sum',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: "Sum"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('quote text value suggestion close quotes', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "Sum"',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: "Sum"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('tick text value suggestion close quotes', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary `Sum');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary `Sum`',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: `Sum`',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag summary value', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --summary ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --summary',
|
||||
Description: 'The Jira issue summary',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
Suggestion: 'summary: ""',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag project dynamic select value', async () => {
|
||||
const f = Client4.executeAppCall;
|
||||
Client4.executeAppCall = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
|
||||
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project ');
|
||||
Client4.executeAppCall = f;
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project special-value',
|
||||
Suggestion: 'special-value',
|
||||
Description: 'special-label',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('create flag epic static select value', async () => {
|
||||
let suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic ');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic1',
|
||||
Suggestion: 'Dylan Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
|
||||
Suggestion: 'Michael Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic M');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
|
||||
Suggestion: 'Michael Epic',
|
||||
Description: 'The Jira epic',
|
||||
Hint: 'The thing is working great!',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic Nope');
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic',
|
||||
Suggestion: '',
|
||||
Description: '',
|
||||
Hint: 'No matching options.',
|
||||
IconData: 'error',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('filled out form shows execute', async () => {
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true ');
|
||||
|
||||
if (!checkForExecuteSuggestion) {
|
||||
expect(suggestions).toEqual([]);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true _execute_current_command',
|
||||
Suggestion: 'Execute Current Command',
|
||||
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
|
||||
IconData: '_execute_current_command',
|
||||
Hint: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeCallFromCommand', () => {
|
||||
const base = {
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
channel_id: 'current_channel_id',
|
||||
location: '/command',
|
||||
root_id: 'root_id',
|
||||
team_id: 'team_id',
|
||||
},
|
||||
path: '/create-issue',
|
||||
};
|
||||
|
||||
test('empty form', async () => {
|
||||
const cmd = '/jira issue create';
|
||||
const values = {};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
...base,
|
||||
raw_command: cmd,
|
||||
expand: {},
|
||||
query: undefined,
|
||||
selected_field: undefined,
|
||||
values,
|
||||
});
|
||||
});
|
||||
|
||||
test('full form', async () => {
|
||||
const cmd = '/jira issue create --summary "Here it is" --epic epic1 --verbose true --project';
|
||||
const values = {
|
||||
summary: 'Here it is',
|
||||
epic: {
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
verbose: 'true',
|
||||
project: '',
|
||||
};
|
||||
|
||||
const {call} = await parser.composeCallFromCommand(cmd);
|
||||
expect(call).toEqual({
|
||||
...base,
|
||||
expand: {},
|
||||
selected_field: undefined,
|
||||
query: undefined,
|
||||
raw_command: cmd,
|
||||
values,
|
||||
});
|
||||
});
|
||||
|
||||
test('dynamic lookup test', async () => {
|
||||
const f = Client4.executeAppCall;
|
||||
|
||||
const mockedExecute = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
|
||||
Client4.executeAppCall = mockedExecute;
|
||||
|
||||
const suggestions = await parser.getSuggestions('/jira issue create --summary "The summary" --epic epic1 --project special');
|
||||
Client4.executeAppCall = f;
|
||||
|
||||
expect(suggestions).toEqual([
|
||||
{
|
||||
Complete: 'jira issue create --summary "The summary" --epic epic1 --project special-value',
|
||||
Suggestion: 'special-value',
|
||||
Description: 'special-label',
|
||||
Hint: '',
|
||||
IconData: 'Create icon',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(mockedExecute).toHaveBeenCalledWith({
|
||||
context: {
|
||||
app_id: 'jira',
|
||||
channel_id: 'current_channel_id',
|
||||
location: '/command',
|
||||
root_id: 'root_id',
|
||||
team_id: 'team_id',
|
||||
},
|
||||
expand: {},
|
||||
path: '/create-issue',
|
||||
query: 'special',
|
||||
raw_command: '/jira issue create --summary "The summary" --epic epic1 --project special',
|
||||
selected_field: 'project',
|
||||
values: {
|
||||
summary: 'The summary',
|
||||
epic: {
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
},
|
||||
}, AppCallTypes.LOOKUP);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type {
|
||||
AppCallRequest,
|
||||
AppBinding,
|
||||
AppField,
|
||||
AppSelectOption,
|
||||
AppCallResponse,
|
||||
AppCallValues,
|
||||
AppContext,
|
||||
AppForm,
|
||||
AutocompleteElement,
|
||||
AutocompleteDynamicSelect,
|
||||
AutocompleteStaticSelect,
|
||||
AutocompleteUserSelect,
|
||||
AutocompleteChannelSelect,
|
||||
} from '@mm-redux/types/apps';
|
||||
|
||||
import type {
|
||||
AutocompleteSuggestion,
|
||||
} from '@mm-redux/types/integrations';
|
||||
export type {AutocompleteSuggestion};
|
||||
|
||||
export type {
|
||||
Channel,
|
||||
} from '@mm-redux/types/channels';
|
||||
|
||||
export type {
|
||||
GlobalState,
|
||||
} from '@mm-redux/types/store';
|
||||
|
||||
export type {
|
||||
DispatchFunc,
|
||||
} from '@mm-redux/types/actions';
|
||||
|
||||
export {
|
||||
AppBindingLocations,
|
||||
AppCallTypes,
|
||||
AppFieldTypes,
|
||||
AppCallResponseTypes,
|
||||
} from '@mm-redux/constants/apps';
|
||||
|
||||
export {getAppsBindings} from '@mm-redux/selectors/entities/apps';
|
||||
export {getPost} from '@mm-redux/selectors/entities/posts';
|
||||
export {getChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
|
||||
export {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
|
||||
export {getUserByUsername as selectUserByUsername} from '@mm-redux/selectors/entities/users';
|
||||
|
||||
export {getUserByUsername} from '@mm-redux/actions/users';
|
||||
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
|
||||
|
||||
export {doAppCall} from '@actions/apps';
|
||||
export {createCallRequest} from '@utils/apps';
|
||||
|
||||
import Store from '@store/store';
|
||||
export const getStore = () => Store.redux;
|
||||
|
||||
import keyMirror from '@mm-redux/utils/key_mirror';
|
||||
export {keyMirror};
|
||||
|
||||
export const EXECUTE_CURRENT_COMMAND_ITEM_ID = '_execute_current_command';
|
||||
|
||||
import type {ParsedCommand} from './app_command_parser';
|
||||
export const getExecuteSuggestion = (_: ParsedCommand): AutocompleteSuggestion | null => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return null;
|
||||
};
|
||||
|
||||
import {Alert} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
export const displayError = (intl: typeof intlShape, body: string) => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.general.error.title',
|
||||
defaultMessage: 'Error',
|
||||
});
|
||||
Alert.alert(title, body);
|
||||
};
|
||||
|
||||
export const errorMessage = (intl: typeof intlShape, error: string, _command: string, _position: number): string => { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
return intl.formatMessage({
|
||||
id: 'apps.error.parser',
|
||||
defaultMessage: 'Parsing error: {error}',
|
||||
}, {
|
||||
error,
|
||||
});
|
||||
};
|
||||
@@ -1,228 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {
|
||||
AppBinding,
|
||||
AppForm,
|
||||
AppFieldTypes,
|
||||
} from './app_command_parser_test_dependencies';
|
||||
|
||||
export const reduxTestState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'current_channel_id',
|
||||
myMembers: {
|
||||
current_channel_id: {
|
||||
channel_id: 'current_channel_id',
|
||||
user_id: 'current_user_id',
|
||||
roles: 'channel_role',
|
||||
mention_count: 1,
|
||||
msg_count: 9,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
current_channel_id: {
|
||||
id: 'current_channel_id',
|
||||
name: 'default-name',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
total_msg_count: 10,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
current_user_id__existingId: {
|
||||
id: 'current_user_id__existingId',
|
||||
name: 'current_user_id__existingId',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: '0',
|
||||
total_msg_count: 0,
|
||||
team_id: 'team_id',
|
||||
},
|
||||
},
|
||||
channelsInTeam: {
|
||||
'team-id': ['current_channel_id'],
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team_id',
|
||||
name: 'team-1',
|
||||
displayName: 'Team 1',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
'team-id': {roles: 'team_role'},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
'display_settings--name_format': {
|
||||
category: 'display_settings',
|
||||
name: 'name_format',
|
||||
user_id: 'current_user_id',
|
||||
value: 'username',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_role: {
|
||||
permissions: [],
|
||||
},
|
||||
team_role: {
|
||||
permissions: [],
|
||||
},
|
||||
channel_role: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
license: {IsLicensed: 'false'},
|
||||
serverVersion: '5.25.0',
|
||||
config: {PostEditTimeLimit: -1},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const viewCommand: AppBinding = {
|
||||
app_id: 'jira',
|
||||
label: 'view',
|
||||
location: 'view',
|
||||
description: 'View details of a Jira issue',
|
||||
form: {
|
||||
call: {
|
||||
path: '/view-issue',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'project',
|
||||
label: 'project',
|
||||
description: 'The Jira project description',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
is_required: true,
|
||||
},
|
||||
{
|
||||
name: 'issue',
|
||||
position: 1,
|
||||
description: 'The Jira issue key',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'MM-11343',
|
||||
is_required: true,
|
||||
},
|
||||
],
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const createCommand: AppBinding = {
|
||||
app_id: 'jira',
|
||||
label: 'create',
|
||||
location: 'create',
|
||||
description: 'Create a new Jira issue',
|
||||
icon: 'Create icon',
|
||||
hint: 'Create hint',
|
||||
form: {
|
||||
call: {
|
||||
path: '/create-issue',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'project',
|
||||
label: 'project',
|
||||
description: 'The Jira project description',
|
||||
type: AppFieldTypes.DYNAMIC_SELECT,
|
||||
hint: 'The Jira project hint',
|
||||
},
|
||||
{
|
||||
name: 'summary',
|
||||
label: 'summary',
|
||||
description: 'The Jira issue summary',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'The thing is working great!',
|
||||
},
|
||||
{
|
||||
name: 'verbose',
|
||||
label: 'verbose',
|
||||
description: 'display details',
|
||||
type: AppFieldTypes.BOOL,
|
||||
hint: 'yes or no!',
|
||||
},
|
||||
{
|
||||
name: 'epic',
|
||||
label: 'epic',
|
||||
description: 'The Jira epic',
|
||||
type: AppFieldTypes.STATIC_SELECT,
|
||||
hint: 'The thing is working great!',
|
||||
options: [
|
||||
{
|
||||
label: 'Dylan Epic',
|
||||
value: 'epic1',
|
||||
},
|
||||
{
|
||||
label: 'Michael Epic',
|
||||
value: 'epic2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as AppForm,
|
||||
};
|
||||
|
||||
export const testBindings: AppBinding[] = [
|
||||
{
|
||||
app_id: '',
|
||||
label: '',
|
||||
location: '/command',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'jira',
|
||||
label: 'jira',
|
||||
description: 'Interact with your Jira instance',
|
||||
icon: 'Jira icon',
|
||||
hint: 'Jira hint',
|
||||
bindings: [{
|
||||
app_id: 'jira',
|
||||
label: 'issue',
|
||||
description: 'Interact with Jira issues',
|
||||
icon: 'Issue icon',
|
||||
hint: 'Issue hint',
|
||||
bindings: [
|
||||
viewCommand,
|
||||
createCommand,
|
||||
],
|
||||
}],
|
||||
},
|
||||
{
|
||||
app_id: 'other',
|
||||
label: 'other',
|
||||
description: 'Other description',
|
||||
icon: 'Other icon',
|
||||
hint: 'Other hint',
|
||||
bindings: [{
|
||||
app_id: 'other',
|
||||
label: 'sub1',
|
||||
description: 'Some Description',
|
||||
form: {
|
||||
fields: [{
|
||||
name: 'fieldname',
|
||||
label: 'fieldlabel',
|
||||
description: 'field description',
|
||||
type: AppFieldTypes.TEXT,
|
||||
hint: 'field hint',
|
||||
}],
|
||||
},
|
||||
}],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import thunk from 'redux-thunk';
|
||||
export {thunk};
|
||||
|
||||
const configureStore = require('redux-mock-store').default;
|
||||
export {configureStore};
|
||||
|
||||
export {Client4} from '@client/rest';
|
||||
|
||||
export type {AppBinding, AppForm} from '@mm-redux/types/apps';
|
||||
export {AppFieldTypes} from '@mm-redux/constants/apps';
|
||||
|
||||
export const checkForExecuteSuggestion = false;
|
||||
@@ -1,18 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-redux/actions/integrations';
|
||||
import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
import SlashSuggestion from './slash_suggestion';
|
||||
|
||||
// TODO: Remove when all below commands have been implemented
|
||||
@@ -28,17 +25,16 @@ const mobileCommandsSelector = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
commands: mobileCommandsSelector(state),
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state),
|
||||
suggestions: getCommandAutocompleteSuggestionsList(state),
|
||||
appsEnabled: appsEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getAutocompleteCommands,
|
||||
@@ -1,83 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {analytics} from '@init/analytics';
|
||||
import {Client4} from '@client/rest';
|
||||
import {analytics} from '@init/analytics.ts';
|
||||
import {Client4} from '@mm-redux/client';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {Command, AutocompleteSuggestion, CommandArgs} from '@mm-redux/types/integrations';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import SlashSuggestionItem from './slash_suggestion_item';
|
||||
import {AppCommandParser} from './app_command_parser/app_command_parser';
|
||||
|
||||
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
|
||||
|
||||
export type Props = {
|
||||
actions: {
|
||||
getAutocompleteCommands: (channelID: string) => void;
|
||||
getCommandAutocompleteSuggestions: (value: string, teamID: string, args: CommandArgs) => void;
|
||||
export default class SlashSuggestion extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getAutocompleteCommands: PropTypes.func.isRequired,
|
||||
getCommandAutocompleteSuggestions: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
commands: PropTypes.array,
|
||||
isSearch: PropTypes.bool,
|
||||
maxListHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onChangeText: PropTypes.func.isRequired,
|
||||
onResultCountChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
nestedScrollEnabled: PropTypes.bool,
|
||||
suggestions: PropTypes.array,
|
||||
rootId: PropTypes.string,
|
||||
channelId: PropTypes.string,
|
||||
};
|
||||
currentTeamId: string;
|
||||
commands: Command[];
|
||||
isSearch?: boolean;
|
||||
maxListHeight?: number;
|
||||
theme: Theme;
|
||||
onChangeText: (text: string) => void;
|
||||
onResultCountChange: (count: number) => void;
|
||||
value: string;
|
||||
nestedScrollEnabled?: boolean;
|
||||
suggestions: AutocompleteSuggestion[];
|
||||
rootId?: string;
|
||||
channelId: string;
|
||||
appsEnabled: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
active: boolean;
|
||||
dataSource: AutocompleteSuggestion[];
|
||||
lastCommandRequest: number;
|
||||
}
|
||||
|
||||
export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
defaultChannel: {},
|
||||
value: '',
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
appCommandParser: AppCommandParser;
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
dataSource: [],
|
||||
lastCommandRequest: 0,
|
||||
};
|
||||
|
||||
constructor(props: Props, context: any) {
|
||||
super(props);
|
||||
this.appCommandParser = new AppCommandParser(null, context.intl, props.channelId, props.rootId);
|
||||
}
|
||||
|
||||
setActive(active: boolean) {
|
||||
setActive(active) {
|
||||
this.setState({active});
|
||||
}
|
||||
|
||||
setLastCommandRequest(lastCommandRequest: number) {
|
||||
setLastCommandRequest(lastCommandRequest) {
|
||||
this.setState({lastCommandRequest});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
componentDidUpdate(prevProps) {
|
||||
if ((this.props.value === prevProps.value && this.props.suggestions === prevProps.suggestions && this.props.commands === prevProps.commands) ||
|
||||
this.props.isSearch || this.props.value.startsWith('//') || !this.props.channelId) {
|
||||
return;
|
||||
@@ -109,23 +88,25 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
this.setLastCommandRequest(Date.now());
|
||||
}
|
||||
|
||||
this.showBaseCommands(nextValue, nextCommands, prevProps.channelId, prevProps.rootId);
|
||||
const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
|
||||
this.updateSuggestions(matches);
|
||||
} else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
|
||||
// If this is an app command, then hand it off to the app command parser.
|
||||
if (this.props.appsEnabled && this.isAppCommand(nextValue, prevProps.channelId, prevProps.rootId)) {
|
||||
this.fetchAndShowAppCommandSuggestions(nextValue, prevProps.channelId, prevProps.rootId);
|
||||
} else if (nextSuggestions === prevProps.suggestions) {
|
||||
if (nextSuggestions === prevProps.suggestions) {
|
||||
const args = {
|
||||
channel_id: prevProps.channelId,
|
||||
team_id: prevProps.currentTeamId,
|
||||
...(prevProps.rootId && {root_id: prevProps.rootId, parent_id: prevProps.rootId}),
|
||||
};
|
||||
this.props.actions.getCommandAutocompleteSuggestions(nextValue, nextTeamId, args);
|
||||
} else {
|
||||
const matches: AutocompleteSuggestion[] = [];
|
||||
nextSuggestions.forEach((suggestion: AutocompleteSuggestion) => {
|
||||
if (!this.contains(matches, '/' + suggestion.Complete)) {
|
||||
matches.push(suggestion);
|
||||
const matches = [];
|
||||
nextSuggestions.forEach((sug) => {
|
||||
if (!this.contains(matches, '/' + sug.Complete)) {
|
||||
matches.push({
|
||||
Complete: sug.Complete,
|
||||
Suggestion: sug.Suggestion,
|
||||
Hint: sug.Hint,
|
||||
Description: sug.Description,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.updateSuggestions(matches);
|
||||
@@ -135,52 +116,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
showBaseCommands = (text: string, commands: Command[], channelID: string, rootID?: string) => {
|
||||
let matches: AutocompleteSuggestion[] = [];
|
||||
|
||||
if (this.props.appsEnabled) {
|
||||
const appCommands = this.getAppBaseCommandSuggestions(text, channelID, rootID);
|
||||
matches = matches.concat(appCommands);
|
||||
}
|
||||
|
||||
matches = matches.concat(this.filterCommands(text.substring(1), commands));
|
||||
|
||||
matches.sort((match1, match2) => {
|
||||
if (match1.Suggestion === match2.Suggestion) {
|
||||
return 0;
|
||||
}
|
||||
return match1.Suggestion > match2.Suggestion ? 1 : -1;
|
||||
});
|
||||
|
||||
this.updateSuggestions(matches);
|
||||
}
|
||||
|
||||
isAppCommand = (pretext: string, channelID: string, rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
return this.appCommandParser.isAppCommand(pretext);
|
||||
}
|
||||
|
||||
fetchAndShowAppCommandSuggestions = async (pretext: string, channelID: string, rootID?: string) => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
const suggestions = await this.appCommandParser.getSuggestions(pretext);
|
||||
this.updateSuggestions(suggestions);
|
||||
}
|
||||
|
||||
getAppBaseCommandSuggestions = (pretext: string, channelID: string, rootID?: string): AutocompleteSuggestion[] => {
|
||||
this.appCommandParser.setChannelContext(channelID, rootID);
|
||||
const suggestions = this.appCommandParser.getSuggestionsBase(pretext);
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
updateSuggestions = (matches: AutocompleteSuggestion[]) => {
|
||||
updateSuggestions = (matches) => {
|
||||
this.setState({
|
||||
active: Boolean(matches.length),
|
||||
active: matches.length,
|
||||
dataSource: matches,
|
||||
});
|
||||
this.props.onResultCountChange(matches.length);
|
||||
}
|
||||
|
||||
filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
|
||||
filterSlashSuggestions = (matchTerm, commands) => {
|
||||
const data = commands.filter((command) => {
|
||||
if (!command.auto_complete) {
|
||||
return false;
|
||||
@@ -196,16 +140,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
Suggestion: '/' + item.trigger,
|
||||
Hint: item.auto_complete_hint,
|
||||
Description: item.auto_complete_desc,
|
||||
IconData: item.icon_url,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
contains = (matches: AutocompleteSuggestion[], complete: string): boolean => {
|
||||
return matches.findIndex((match) => match.Complete === complete) !== -1;
|
||||
contains = (matches, complete) => {
|
||||
return matches.findIndex((match) => match.complete === complete) !== -1;
|
||||
}
|
||||
|
||||
completeSuggestion = (command: string) => {
|
||||
completeSuggestion = (command) => {
|
||||
const {onChangeText} = this.props;
|
||||
analytics.trackCommand('complete_suggestion', `/${command} `);
|
||||
|
||||
@@ -233,9 +176,9 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id || item.Suggestion;
|
||||
keyExtractor = (item) => item.id || item.Suggestion;
|
||||
|
||||
renderItem = ({item}: {item: AutocompleteSuggestion}) => (
|
||||
renderItem = ({item}) => (
|
||||
<SlashSuggestionItem
|
||||
description={item.Description}
|
||||
hint={item.Hint}
|
||||
@@ -243,7 +186,6 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
theme={this.props.theme}
|
||||
suggestion={item.Suggestion}
|
||||
complete={item.Complete}
|
||||
icon={item.IconData}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -266,15 +208,16 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
|
||||
extraData={this.state}
|
||||
data={this.state.dataSource}
|
||||
keyExtractor={this.keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
pageSize={10}
|
||||
initialListSize={10}
|
||||
nestedScrollEnabled={nestedScrollEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
flex: 1,
|
||||
@@ -1,263 +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 {
|
||||
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}/>);
|
||||
|
||||
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}/>);
|
||||
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}/>);
|
||||
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}/>);
|
||||
|
||||
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}/>);
|
||||
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}/>);
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
@@ -2,18 +2,15 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Image, Text, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
import slashIcon from '@assets/images/autocomplete/slash_command.png';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
const slashIcon = require('@assets/images/autocomplete/slash_command.png');
|
||||
const bangIcon = require('@assets/images/autocomplete/slash_command_error.png');
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
icon: {
|
||||
fontSize: 24,
|
||||
@@ -51,17 +48,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
complete: string;
|
||||
description: string;
|
||||
hint: string;
|
||||
onPress: (complete: string) => void;
|
||||
suggestion: string;
|
||||
icon: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const SlashSuggestionItem = (props: Props) => {
|
||||
const SlashSuggestionItem = (props) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const {
|
||||
complete,
|
||||
@@ -83,40 +70,6 @@ const SlashSuggestionItem = (props: Props) => {
|
||||
suggestionText = suggestionText.substring(1);
|
||||
}
|
||||
|
||||
if (hint) {
|
||||
if (suggestionText.length) {
|
||||
suggestionText += ` ${hint}`;
|
||||
} else {
|
||||
suggestionText = hint;
|
||||
}
|
||||
}
|
||||
|
||||
let image = (
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={slashIcon}
|
||||
/>
|
||||
);
|
||||
if (props.icon === 'error') {
|
||||
image = (
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={bangIcon}
|
||||
/>
|
||||
);
|
||||
} else if (props.icon && props.icon.startsWith('http')) {
|
||||
image = (
|
||||
<FastImage
|
||||
source={{uri: props.icon}}
|
||||
style={{width: 16, height: 16}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={completeSuggestion}
|
||||
@@ -126,10 +79,15 @@ const SlashSuggestionItem = (props: Props) => {
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.icon}>
|
||||
{image}
|
||||
<Image
|
||||
style={style.iconColor}
|
||||
width={10}
|
||||
height={16}
|
||||
source={slashIcon}
|
||||
/>
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText}`}</Text>
|
||||
<Text style={style.suggestionName}>{`${suggestionText} ${hint}`}</Text>
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
@@ -143,4 +101,13 @@ const SlashSuggestionItem = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
SlashSuggestionItem.propTypes = {
|
||||
description: PropTypes.string,
|
||||
hint: PropTypes.string,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
suggestion: PropTypes.string,
|
||||
complete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SlashSuggestionItem;
|
||||
@@ -21,7 +21,6 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
actions: PropTypes.shape({
|
||||
setAutocompleteSelector: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getDynamicOptions: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.string,
|
||||
@@ -99,11 +98,11 @@ export default class AutocompleteSelector extends PureComponent {
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions} = this.props;
|
||||
const {actions, dataSource, options, placeholder} = this.props;
|
||||
const screen = 'SelectorScreen';
|
||||
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options, getDynamicOptions);
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options);
|
||||
goToScreen(screen, title);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,36 +28,6 @@ exports[`Avatars should match snapshot for overflow 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={24}
|
||||
testID="avatars.profile_picture"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
userId="user1"
|
||||
/>
|
||||
</View>
|
||||
@@ -79,36 +49,6 @@ exports[`Avatars should match snapshot for overflow 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={24}
|
||||
testID="avatars.profile_picture"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
userId="user2"
|
||||
/>
|
||||
</View>
|
||||
@@ -130,36 +70,6 @@ exports[`Avatars should match snapshot for overflow 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={24}
|
||||
testID="avatars.profile_picture"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
userId="user3"
|
||||
/>
|
||||
</View>
|
||||
@@ -237,36 +147,6 @@ exports[`Avatars should match snapshot for single avatar 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
showStatus={false}
|
||||
size={24}
|
||||
testID="avatars.profile_picture"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
userId="user1"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -108,8 +108,6 @@ export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
userId={userId}
|
||||
size={ViewTypes.AVATAR_LIST_PICTURE_SIZE}
|
||||
showStatus={false}
|
||||
testID='avatars.profile_picture'
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
|
||||
236
app/components/channel_icon.js
Normal file
236
app/components/channel_icon.js
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export default class ChannelIcon extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
hasDraft: PropTypes.bool,
|
||||
membersCount: PropTypes.number,
|
||||
size: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
isArchived: PropTypes.bool.isRequired,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isActive: false,
|
||||
isInfo: false,
|
||||
isUnread: false,
|
||||
size: 12,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isActive,
|
||||
isUnread,
|
||||
isInfo,
|
||||
hasDraft,
|
||||
membersCount,
|
||||
size,
|
||||
status,
|
||||
theme,
|
||||
type,
|
||||
isArchived,
|
||||
isBot,
|
||||
testID,
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let activeIcon;
|
||||
let unreadIcon;
|
||||
let activeGroupBox;
|
||||
let unreadGroupBox;
|
||||
let activeGroup;
|
||||
let unreadGroup;
|
||||
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
|
||||
|
||||
if (isUnread) {
|
||||
unreadIcon = style.iconUnread;
|
||||
unreadGroupBox = style.groupBoxUnread;
|
||||
unreadGroup = style.groupUnread;
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
activeIcon = style.iconActive;
|
||||
activeGroupBox = style.groupBoxActive;
|
||||
activeGroup = style.groupActive;
|
||||
}
|
||||
|
||||
if (isInfo) {
|
||||
activeIcon = style.iconInfo;
|
||||
activeGroupBox = style.groupBoxInfo;
|
||||
activeGroup = style.groupInfo;
|
||||
offlineColor = changeOpacity(theme.centerChannelColor, 0.5);
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (isArchived) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='archive-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
testID={`${testID}.archive`}
|
||||
/>
|
||||
);
|
||||
} else if (isBot) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='robot-happy'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, left: -1.5, top: -1}]}
|
||||
testID={`${testID}.bot`}
|
||||
/>
|
||||
);
|
||||
} else if (hasDraft) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
testID={`${testID}.draft`}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size}]}
|
||||
testID={`${testID}.public`}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='lock-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, left: 0.5}]}
|
||||
testID={`${testID}.private`}
|
||||
/>
|
||||
);
|
||||
} else if (type === General.GM_CHANNEL) {
|
||||
const fontSize = (size - 10);
|
||||
icon = (
|
||||
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: size, height: size}]}>
|
||||
<Text
|
||||
style={[style.group, unreadGroup, activeGroup, {fontSize}]}
|
||||
testID={`${testID}.gm_member_count`}
|
||||
>
|
||||
{membersCount}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (type === General.DM_CHANNEL) {
|
||||
switch (status) {
|
||||
case General.AWAY:
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='clock'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.awayIndicator}]}
|
||||
testID={`${testID}.away`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case General.DND:
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='minus-circle'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.dndIndicator}]}
|
||||
testID={`${testID}.dnd`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case General.ONLINE:
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='check-circle'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: theme.onlineIndicator}]}
|
||||
testID={`${testID}.online`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='circle-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: size, color: offlineColor}]}
|
||||
testID={`${testID}.offline`}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style.container, {height: size}]}>
|
||||
{icon}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginRight: 8,
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
},
|
||||
iconActive: {
|
||||
color: theme.sidebarTextActiveColor,
|
||||
},
|
||||
iconUnread: {
|
||||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
iconInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
groupBox: {
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.3),
|
||||
borderColor: changeOpacity(theme.sidebarText, 0.3),
|
||||
borderWidth: 1,
|
||||
borderRadius: 2,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
groupBoxActive: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
|
||||
},
|
||||
groupBoxUnread: {
|
||||
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
|
||||
},
|
||||
groupBoxInfo: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
},
|
||||
group: {
|
||||
color: changeOpacity(theme.sidebarText, 0.6),
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
groupActive: {
|
||||
color: theme.sidebarTextActiveColor,
|
||||
},
|
||||
groupUnread: {
|
||||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
groupInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,184 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {Theme} from '@mm-redux/types/preferences';
|
||||
|
||||
type Props = {
|
||||
hasDraft: boolean;
|
||||
isActive: boolean;
|
||||
isArchived: boolean;
|
||||
isInfo: boolean;
|
||||
isUnread: boolean;
|
||||
membersCount: number;
|
||||
size: number;
|
||||
statusStyle?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
theme: Theme;
|
||||
type: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.sidebarText, 0.4),
|
||||
},
|
||||
iconActive: {
|
||||
color: theme.sidebarTextActiveColor,
|
||||
},
|
||||
iconUnread: {
|
||||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
iconInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
groupBox: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: changeOpacity(theme.sidebarText, 0.16),
|
||||
borderRadius: 4,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
groupBoxActive: {
|
||||
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.3),
|
||||
},
|
||||
groupBoxUnread: {
|
||||
backgroundColor: changeOpacity(theme.sidebarUnreadText, 0.3),
|
||||
},
|
||||
groupBoxInfo: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.3),
|
||||
},
|
||||
group: {
|
||||
color: theme.sidebarText,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
groupActive: {
|
||||
color: theme.sidebarTextActiveColor,
|
||||
},
|
||||
groupUnread: {
|
||||
color: theme.sidebarUnreadText,
|
||||
},
|
||||
groupInfo: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const ChannelIcon = (props: Props) => {
|
||||
const style = getStyleSheet(props.theme);
|
||||
|
||||
let activeIcon;
|
||||
let unreadIcon;
|
||||
let activeGroupBox;
|
||||
let unreadGroupBox;
|
||||
let activeGroup;
|
||||
let unreadGroup;
|
||||
|
||||
if (props.isUnread) {
|
||||
unreadIcon = style.iconUnread;
|
||||
unreadGroupBox = style.groupBoxUnread;
|
||||
unreadGroup = style.groupUnread;
|
||||
}
|
||||
|
||||
if (props.isActive) {
|
||||
activeIcon = style.iconActive;
|
||||
activeGroupBox = style.groupBoxActive;
|
||||
activeGroup = style.groupActive;
|
||||
}
|
||||
|
||||
if (props.isInfo) {
|
||||
activeIcon = style.iconInfo;
|
||||
activeGroupBox = style.groupBoxInfo;
|
||||
activeGroup = style.groupInfo;
|
||||
}
|
||||
|
||||
let icon;
|
||||
let extraStyle;
|
||||
if (props.isArchived) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='archive-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 1}]}
|
||||
testID={`${props.testID}.archive`}
|
||||
/>
|
||||
);
|
||||
} else if (props.hasDraft) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='pencil-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 2}]}
|
||||
testID={`${props.testID}.draft`}
|
||||
/>
|
||||
);
|
||||
} else if (props.type === General.OPEN_CHANNEL) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='globe'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 1}]}
|
||||
testID={`${props.testID}.public`}
|
||||
/>
|
||||
);
|
||||
} else if (props.type === General.PRIVATE_CHANNEL) {
|
||||
icon = (
|
||||
<CompassIcon
|
||||
name='lock-outline'
|
||||
style={[style.icon, unreadIcon, activeIcon, {fontSize: props.size, left: 0.5}]}
|
||||
testID={`${props.testID}.private`}
|
||||
/>
|
||||
);
|
||||
} else if (props.type === General.GM_CHANNEL) {
|
||||
const fontSize = (props.size - 12);
|
||||
const boxSize = (props.size - 4);
|
||||
icon = (
|
||||
<View style={[style.groupBox, unreadGroupBox, activeGroupBox, {width: boxSize, height: boxSize}]}>
|
||||
<Text
|
||||
style={[style.group, unreadGroup, activeGroup, {fontSize}]}
|
||||
testID={`${props.testID}.gm_member_count`}
|
||||
>
|
||||
{props.membersCount}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (props.type === General.DM_CHANNEL) {
|
||||
// extraStyle = {marginRight: 6};
|
||||
icon = (
|
||||
<ProfilePicture
|
||||
size={props.size}
|
||||
statusSize={12}
|
||||
userId={props.userId}
|
||||
testID={props.testID}
|
||||
statusStyle={props.statusStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[style.container, extraStyle, {width: props.size, height: props.size}]}>
|
||||
{icon}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ChannelIcon.defaultProps = {
|
||||
hasDraft: false,
|
||||
isActive: false,
|
||||
isArchived: false,
|
||||
isInfo: false,
|
||||
isUnread: false,
|
||||
membersCount: 0,
|
||||
size: 12,
|
||||
};
|
||||
|
||||
export default ChannelIcon;
|
||||
@@ -92,7 +92,6 @@ class ChannelIntro extends PureComponent {
|
||||
iconSize={48}
|
||||
statusBorderWidth={2}
|
||||
statusSize={25}
|
||||
testID='channel_intro.profile_picture'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
));
|
||||
|
||||
@@ -6,9 +6,10 @@ import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {popToRoot, dismissAllModals} from '@actions/navigation';
|
||||
import {t} from '@utils/i18n';
|
||||
import {alertErrorWithFallback} from '@utils/general';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {popToRoot, dismissAllModals} from 'app/actions/navigation';
|
||||
|
||||
import {getChannelFromChannelName} from './channel_link_utils';
|
||||
|
||||
@@ -18,9 +19,9 @@ export default class ChannelLink extends React.PureComponent {
|
||||
channelMentions: PropTypes.object,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
linkStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
linkStyle: CustomPropTypes.Style,
|
||||
onChannelLinkPress: PropTypes.func,
|
||||
textStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
textStyle: CustomPropTypes.Style,
|
||||
channelsByName: PropTypes.object.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as RNPlaceholder from 'rn-placeholder';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
import {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -38,7 +39,7 @@ export default class ChannelLoader extends PureComponent {
|
||||
static propTypes = {
|
||||
backgroundColor: PropTypes.string,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
style: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
|
||||
style: CustomPropTypes.Style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
height: PropTypes.number,
|
||||
retryLoad: PropTypes.func,
|
||||
|
||||
@@ -5,10 +5,12 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
export default class ConditionalTouchable extends React.PureComponent {
|
||||
static propTypes = {
|
||||
touchable: PropTypes.bool,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]),
|
||||
children: CustomPropTypes.Children.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ConditionalTouchable from '@components/conditional_touchable';
|
||||
import CustomPropTypes from '@constants/custom_prop_types';
|
||||
|
||||
export default class CustomListRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
@@ -17,7 +18,7 @@ export default class CustomListRow extends React.PureComponent {
|
||||
enabled: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf([PropTypes.node])]),
|
||||
children: CustomPropTypes.Children,
|
||||
testID: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
iconSize={24}
|
||||
size={32}
|
||||
testID="custom_list.user_item.21345.profile_picture"
|
||||
userId="21345"
|
||||
/>
|
||||
</View>
|
||||
@@ -165,7 +164,6 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
<Connect(ProfilePicture)
|
||||
iconSize={24}
|
||||
size={32}
|
||||
testID="custom_list.user_item.21345.profile_picture"
|
||||
userId="21345"
|
||||
/>
|
||||
</View>
|
||||
@@ -299,7 +297,6 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
iconSize={24}
|
||||
size={32}
|
||||
testID="custom_list.user_item.21345.profile_picture"
|
||||
userId="21345"
|
||||
/>
|
||||
</View>
|
||||
@@ -446,7 +443,6 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
<Connect(ProfilePicture)
|
||||
iconSize={24}
|
||||
size={32}
|
||||
testID="custom_list.user_item.21345.profile_picture"
|
||||
userId="21345"
|
||||
/>
|
||||
</View>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user