Compare commits

..

7 Commits

Author SHA1 Message Date
Mattermost Build
121656038c Bump app build number to 347 (#5224) (#5225)
(cherry picked from commit 09386d9fde)

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

Currently translated at 99.5% (659 of 662 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (662 of 662 strings)

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

* Translated using Weblate (Korean)

Currently translated at 97.2% (644 of 662 strings)

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

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

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

* Apply sanitization after getting uri last path segment

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

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

* fix api key file path

* iOS API key from p8 file instead of json

* Properly split api key on newlines

(cherry picked from commit d68cc9c5e1)

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

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

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

View File

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

View File

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

View File

@@ -8,6 +8,10 @@
; Ignore polyfills
node_modules/react-native/Libraries/polyfills/.*
; These should not be required directly
; require from fbjs/lib instead: require('fbjs/lib/warning')
node_modules/warning/.*
; Flow doesn't support platforms
.*/Libraries/Utilities/LoadingView.js
@@ -26,8 +30,6 @@ emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
exact_by_default=true
module.file_ext=.js
module.file_ext=.json
module.file_ext=.ios.js
@@ -42,6 +44,10 @@ suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
[lints]
sketchy-null-number=warn
sketchy-null-mixed=warn
@@ -53,6 +59,7 @@ unsafe-getters-setters=warn
inexact-spread=warn
unnecessary-invariant=warn
signature-verification-failure=warn
deprecated-utility=error
[strict]
deprecated-type
@@ -64,4 +71,4 @@ untyped-import
untyped-type-import
[version]
^0.137.0
^0.122.0

7
.gitattributes vendored
View File

@@ -1,3 +1,4 @@
# Windows files should use crlf line endings
# https://help.github.com/articles/dealing-with-line-endings/
*.bat text eol=crlf
*.pbxproj -text
# specific for windows script files
*.bat text eol=crlf

View File

@@ -1,32 +0,0 @@
Per Mattermost guidelines, GitHub issues are for bug reports: <http://www.mattermost.org/filing-issues/>.
For troubleshooting see: http://forum.mattermost.org/.
For feature proposals see: http://www.mattermost.org/feature-requests/
If you've found a bug--something appears unintentional--please follow these steps:
1. Confirm youre filing a new issue. [Search existing tickets in Jira](https://mattermost.atlassian.net/jira/software/c/projects/MM/issues/) to ensure that the ticket does not already exist.
2. Confirm your issue does not involve security. Otherwise, please see our [Responsible Disclosure Policy](https://about.mattermost.com/report-security-issue/).
3. [File a new issue](https://github.com/mattermost/mattermost-mobile/issues/new) using the format below. Mattermost will confirm steps to reproduce and file in Jira, or ask for more details if there is trouble reproducing it. If there's already an existing bug in Jira, it will be linked back to the GitHub issue so you can track when it gets fixed.
#### Summary
Bug report in one concise sentence
### Environment Information
- Device Name:
- OS Version:
- Mattermost App Version:
- Mattermost Server Version:
#### Steps to reproduce
How can we reproduce the issue (what version are you using?)
#### Expected behavior
Describe your issue in detail
#### Observed behavior (that appears unintentional)
What did you see happen? Please include relevant error messages, screenshots and/or video recordings.
#### Possible fixes
If you can, link to the line of code that might be responsible for the problem

View File

@@ -1,61 +0,0 @@
<!-- Thank you for contributing a pull request! Here are a few tips to help you:
1. If this is your first contribution, make sure you've read the Contribution Checklist https://developers.mattermost.com/contribute/getting-started/contribution-checklist/
2. Read our blog post about "Submitting Great PRs" https://developers.mattermost.com/blog/2019-01-24-submitting-great-prs
3. Take a look at other repository specific documentation at https://developers.mattermost.com/contribute
-->
#### Summary
<!--
A brief description of what this pull request does.
-->
#### Ticket Link
<!--
If this pull request addresses a Help Wanted ticket or fixes a reported issue, please link the relevant GitHub issue, e.g.
Fixes https://github.com/mattermost/mattermost-mobile/issues/XXXXX
Otherwise, link the JIRA ticket.
-->
#### Checklist
<!--
Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.
-->
- [ ] Added or updated unit tests (required for all new features)
- [ ] Has UI changes
- [ ] Includes text changes and localization file updates
#### Device Information
This PR was tested on: <!-- Device name(s), OS version(s) -->
#### Screenshots
<!--
If the PR includes UI changes, include screenshots/GIFs/Videos (for both iOS and Android if possible).
-->
#### Release Note
<!--
Add a release note for each of the following conditions:
* New features and improvements, including behavioural changes, UI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes
If no release notes are required write NONE. Use past-tense. Newlines are stripped.
Example:
```release-note
Added a new config setting ServiceSettings.FooBar. Added a new column Foo to the Users table.
```
```release-note
NONE
```
-->
```release-note
```

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

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

View File

@@ -694,6 +694,37 @@ SOFTWARE.
---
## core-js
This product contains 'core-js' by Denis Pushkarev.
Modular standard library for JavaScript.
* HOMEPAGE:
* https://github.com/zloirock/core-js
* LICENSE: Copyright (c) 2014-2019 Denis Pushkarev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## deep-equal
This product contains 'deep-equal' by James Halliday.

View File

@@ -1,6 +1,6 @@
# Mattermost Mobile
- **Minimum Server versions:** Current ESR version (5.31.3)
- **Minimum Server versions:** Current ESR version (5.25)
- **Supported iOS versions:** 11+
- **Supported Android versions:** 7.0+

View File

@@ -132,8 +132,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 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 {

View File

@@ -4,10 +4,5 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
</manifest>

View File

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

View File

@@ -16,20 +16,12 @@ import com.mattermost.react_native_interface.KeysReadableArray;
public class MattermostCredentialsHelper {
static final String CURRENT_SERVER_URL = "@currentServerUrl";
static KeychainModule keychainModule;
static AsyncStorageHelper asyncStorage;
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
final KeychainModule keychainModule = new KeychainModule(context);
final AsyncStorageHelper asyncStorage = new AsyncStorageHelper(context);
final ArrayList<String> keys = new ArrayList<String>(1);
keys.add(CURRENT_SERVER_URL);
if (keychainModule == null) {
keychainModule = new KeychainModule(context);
}
if (asyncStorage == null) {
asyncStorage = new AsyncStorageHelper(context);
}
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
@Override
public int size() {

View File

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

View File

@@ -27,7 +27,6 @@ import org.json.JSONException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
@@ -46,7 +45,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
super(reactContext);
mApplication = application;
}
private File tempFolder;
@Override
@@ -133,7 +131,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
String text = "";
String type = "";
String action = "";
String extra = "";
Activity currentActivity = getCurrentActivity();
@@ -142,21 +139,20 @@ public class ShareModule extends ReactContextBaseJavaModule {
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
extra = intent.getStringExtra(Intent.EXTRA_TEXT);
if (type == null) {
type = "";
}
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type) && extra != null) {
map.putString("value", extra);
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
text = intent.getStringExtra(Intent.EXTRA_TEXT);
map.putString("value", text);
map.putString("type", type);
map.putBoolean("isString", true);
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if (uri != null) {
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
if (type.equals("image/*")) {
type = "image/jpeg";
@@ -165,16 +161,17 @@ public class ShareModule extends ReactContextBaseJavaModule {
}
map.putString("type", type);
map.putBoolean("isString", false);
items.pushMap(map);
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : Objects.requireNonNull(uris)) {
for (Uri uri : uris) {
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
map = Arguments.createMap();
map.putString("value", "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri));
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
text = "file://" + filePath;
map.putString("value", text);
type = RealPathUtil.getMimeTypeFromUri(currentActivity, uri);
if (type != null) {
if (type.equals("image/*")) {
type = "image/jpeg";
@@ -185,7 +182,6 @@ public class ShareModule extends ReactContextBaseJavaModule {
type = "application/octet-stream";
}
map.putString("type", type);
map.putBoolean("isString", false);
items.pushMap(map);
}
}
@@ -225,7 +221,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
for (int i = 0; i < files.size(); i++) {
for(int i = 0 ; i < files.size() ; i++) {
ReadableMap file = files.getMap(i);
String filePath = file.getString("fullPath").replaceFirst("file://", "");
File fileInfo = new File(filePath);
@@ -249,7 +245,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
JSONObject responseJson = new JSONObject(responseData);
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
JSONArray file_ids = new JSONArray();
for (int i = 0; i < fileInfoArray.length(); i++) {
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
file_ids.put(fileInfo.getString("id"));
}

View File

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

View File

@@ -1,7 +1,7 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowBackground">@android:color/transparent</item>

View File

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

View File

@@ -2,7 +2,7 @@
buildscript {
ext {
buildToolsVersion = "29.0.3"
buildToolsVersion = "29.0.2"
minSdkVersion = 24
compileSdkVersion = 29
targetSdkVersion = 29
@@ -10,7 +10,6 @@ buildscript {
kotlinVersion = "1.3.61"
firebaseVersion = "21.0.0"
RNNKotlinVersion = kotlinVersion
ndkVersion = "21.1.6352462"
}
repositories {
@@ -20,7 +19,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"

View File

@@ -30,4 +30,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1
FLIPPER_VERSION=0.37.0

Binary file not shown.

View File

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

22
android/gradlew.bat vendored
View File

@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -54,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -64,14 +64,28 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1,165 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
import {Post} from '@mm-redux/types/posts';
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
import {handleGotoLocation} from '@mm-redux/actions/integrations';
import {showModal} from './navigation';
import {Theme} from '@mm-redux/types/preferences';
import CompassIcon from '@components/compass_icon';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import EphemeralStore from '@store/ephemeral_store';
import {makeCallErrorResponse} from '@utils/apps';
import {sendEphemeralPost} from '@actions/views/post';
import {CommandArgs} from '@mm-redux/types/integrations';
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
return async (dispatch, getState) => {
try {
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
const responseType = res.type || AppCallResponseTypes.OK;
switch (responseType) {
case AppCallResponseTypes.OK:
return {data: res};
case AppCallResponseTypes.ERROR:
return {error: res};
case AppCallResponseTypes.FORM: {
if (!res.form) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.form.no_form',
defaultMessage: 'Response type is `form`, but no form was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
const screen = EphemeralStore.getNavigationTopComponentId();
if (type === AppCallTypes.SUBMIT && screen !== 'AppForm') {
showAppForm(res.form, call, getTheme(getState()));
}
return {data: res};
}
case AppCallResponseTypes.NAVIGATE:
if (!res.navigate_to_url) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_url',
defaultMessage: 'Response type is `navigate`, but no url was included in response.',
});
return {error: makeCallErrorResponse(errMsg)};
}
if (type !== AppCallTypes.SUBMIT) {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.navigate.no_submit',
defaultMessage: 'Response type is `navigate`, but the call was not a submission.',
});
return {error: makeCallErrorResponse(errMsg)};
}
dispatch(handleGotoLocation(res.navigate_to_url, intl));
return {data: res};
default: {
const errMsg = intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {
type: responseType,
});
return {error: makeCallErrorResponse(errMsg)};
}
}
} catch (error) {
const errMsg = error.message || intl.formatMessage({
id: 'apps.error.responses.unexpected_error',
defaultMessage: 'Received an unexpected error.',
});
return {error: makeCallErrorResponse(errMsg)};
}
};
}
const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) => {
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
let submitButtons = [{
id: 'submit-form',
showAsAction: 'always',
text: 'Submit',
}];
if (form.submit_buttons) {
const options = form.fields.find((f) => f.name === form.submit_buttons)?.options;
const newButtons = options?.map((o) => {
return {
id: 'submit-form_' + o.value,
showAsAction: 'always',
text: o.label,
};
});
if (newButtons && newButtons.length > 0) {
submitButtons = newButtons;
}
}
const options = {
topBar: {
leftButtons: [{
id: 'close-dialog',
icon: closeButton,
}],
rightButtons: submitButtons,
},
};
const passProps = {form, call};
showModal('AppForm', form.title, passProps, options);
};
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
return (dispatch: DispatchFunc) => {
return dispatch(sendEphemeralPost(
message,
post.channel_id,
post.root_id || post.id,
response.app_metadata?.bot_user_id,
));
};
}
export function postEphemeralCallResponseForChannel(response: AppCallResponse, message: string, channelID: string): ActionFunc {
return (dispatch: DispatchFunc) => {
return dispatch(sendEphemeralPost(
message,
channelID,
'',
response.app_metadata?.bot_user_id,
));
};
}
export function postEphemeralCallResponseForContext(response: AppCallResponse, message: string, context: AppContext): ActionFunc {
return (dispatch: DispatchFunc) => {
return dispatch(sendEphemeralPost(
message,
context.channel_id,
context.root_id || context.post_id,
response.app_metadata?.bot_user_id,
));
};
}
export function postEphemeralCallResponseForCommandArgs(response: AppCallResponse, message: string, args: CommandArgs): ActionFunc {
return (dispatch: DispatchFunc) => {
return dispatch(sendEphemeralPost(
message,
args.channel_id,
args.root_id,
response.app_metadata?.bot_user_id,
));
};
}

View File

@@ -3,7 +3,7 @@
/* eslint-disable no-import-assign */
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {Preferences} from '@mm-redux/constants';
import {PreferenceTypes} from '@mm-redux/action_types';

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {General, Preferences} from '@mm-redux/constants';
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
import {getConfig} from '@mm-redux/selectors/entities/general';

View File

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

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntegrationTypes} from '@mm-redux/action_types';
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
export function executeCommand(message, channelId, rootId) {
return async (dispatch, getState) => {
const state = getState();
const teamId = getCurrentTeamId(state);
const args = {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId,
};
let msg = message;
let cmdLength = msg.indexOf(' ');
if (cmdLength < 0) {
cmdLength = msg.length;
}
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
const {data, error} = await dispatch(executeCommandService(msg, args));
if (data?.trigger_id) { //eslint-disable-line camelcase
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}
return {data, error};
};
}

View File

@@ -1,97 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {intlShape} from 'react-intl';
import {IntegrationTypes} from '@mm-redux/action_types';
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
import {CommandArgs} from '@mm-redux/types/integrations';
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
import {appsEnabled} from '@utils/apps';
import {AppCallResponse} from '@mm-redux/types/apps';
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const teamId = getCurrentTeamId(state);
const args: CommandArgs = {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId,
};
let msg = message;
msg = filterEmDashForCommand(msg);
let cmdLength = msg.indexOf(' ');
if (cmdLength < 0) {
cmdLength = msg.length;
}
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
const appsAreEnabled = appsEnabled(state);
if (appsAreEnabled) {
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.root_id);
if (parser.isAppCommand(msg)) {
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
const createErrorMessage = (errMessage: string) => {
return {error: {message: errMessage}};
};
if (!call) {
return createErrorMessage(errorMessage!);
}
const res = await dispatch(doAppCall(call, AppCallTypes.SUBMIT, intl));
if (res.error) {
const errorResponse = res.error as AppCallResponse;
return createErrorMessage(errorResponse.error || intl.formatMessage({
id: 'apps.error.unknown',
defaultMessage: 'Unknown error.',
}));
}
const callResp = res.data as AppCallResponse;
switch (callResp.type) {
case AppCallResponseTypes.OK:
if (callResp.markdown) {
dispatch(postEphemeralCallResponseForCommandArgs(callResp, callResp.markdown, args));
}
return {data: {}};
case AppCallResponseTypes.FORM:
case AppCallResponseTypes.NAVIGATE:
return {data: {}};
default:
return createErrorMessage(intl.formatMessage({
id: 'apps.error.responses.unknown_type',
defaultMessage: 'App response type not supported. Response type: {type}.',
}, {
type: callResp.type,
}));
}
}
}
const {data, error} = await dispatch(executeCommandService(msg, args));
if (data?.trigger_id) { //eslint-disable-line camelcase
dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id});
}
return {data, error};
};
}
const filterEmDashForCommand = (command: string): string => {
return command.replace(/\u2014/g, '--');
};

View File

@@ -5,7 +5,7 @@ import {batchActions} from 'redux-batched-actions';
import {EmojiTypes} from '@mm-redux/action_types';
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';

View File

@@ -7,7 +7,7 @@ import {getDataRetentionPolicy} from '@mm-redux/actions/general';
import {GeneralTypes} from '@mm-redux/action_types';
import {getSessions} from '@mm-redux/actions/users';
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';

View File

@@ -4,7 +4,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {handleSuccessfulLogin} from 'app/actions/views/login';

View File

@@ -15,7 +15,7 @@ import {
receivedPostsSince,
receivedPostsInThread,
} from '@mm-redux/actions/posts';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {Posts} from '@mm-redux/constants';
import {getPost as selectPost, getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
@@ -28,28 +28,6 @@ import {getChannelSinceValue} from '@utils/channels';
import {getEmojisInPosts} from './emoji';
export function sendEphemeralPost(message, channelId = '', parentId = '', userId = '0') {
return async (dispatch, getState) => {
const timestamp = Date.now();
const post = {
id: generateId(),
user_id: userId,
channel_id: channelId || getCurrentChannelId(getState()),
message,
type: Posts.POST_TYPES.EPHEMERAL,
create_at: timestamp,
update_at: timestamp,
root_id: parentId,
parent_id: parentId,
props: {},
};
dispatch(receivedNewPost(post));
return {};
};
}
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
return async (dispatch) => {
const timestamp = Date.now();
@@ -73,14 +51,13 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
};
}
export function setAutocompleteSelector(dataSource, onSelect, options, getDynamicOptions) {
export function setAutocompleteSelector(dataSource, onSelect, options) {
return {
type: ViewTypes.SELECTED_ACTION_MENU,
data: {
dataSource,
onSelect,
options,
getDynamicOptions,
},
};
}

View File

@@ -6,7 +6,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {PostTypes, UserTypes} from '@mm-redux/action_types';
import * as PostSelectors from '@mm-redux/selectors/entities/posts';

View File

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

View File

@@ -4,7 +4,7 @@
import moment from 'moment-timezone';
import {getSessions} from '@mm-redux/actions/users';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';

View File

@@ -8,7 +8,7 @@ import {GeneralTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
import * as HelperActions from '@mm-redux/actions/helpers';
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';

View File

@@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchAppBindings} from '@mm-redux/actions/apps';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {appsEnabled} from '@utils/apps';
export function handleRefreshAppsBindings() {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
const state = getState();
if (appsEnabled(state)) {
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
}
return {data: true};
};
}

View File

@@ -12,7 +12,7 @@ import configureMockStore from 'redux-mock-store';
import {ChannelTypes, RoleTypes} from '@mm-redux/action_types';
import * as ChannelActions from '@mm-redux/actions/channels';
import * as TeamActions from '@mm-redux/actions/teams';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import * as Actions from '@actions/websocket';

View File

@@ -5,7 +5,7 @@ import {fetchChannelAndMyMember} from '@actions/helpers/channels';
import {loadChannelsForTeam} from '@actions/views/channel';
import {WebsocketEvents} from '@constants';
import {markChannelAsRead} from '@mm-redux/actions/channels';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
import {General} from '@mm-redux/constants';
import {

View File

@@ -5,7 +5,7 @@
import assert from 'assert';
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';

View File

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

View File

@@ -3,7 +3,7 @@
import assert from 'assert';
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';

View File

@@ -9,7 +9,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import * as ChannelActions from '@mm-redux/actions/channels';
import * as PostActions from '@mm-redux/actions/posts';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {General, Posts} from '@mm-redux/constants';
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
import EventEmitter from '@mm-redux/utils/event_emitter';

View File

@@ -4,7 +4,7 @@
import assert from 'assert';
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';

View File

@@ -7,7 +7,7 @@ import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import {batchActions} from 'redux-batched-actions';
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';

View File

@@ -3,7 +3,7 @@
import {RoleTypes, TeamTypes} from '@mm-redux/action_types';
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {getCurrentTeamId, getTeams as getTeamsSelector} from '@mm-redux/selectors/entities/teams';
import {getCurrentUser} from '@mm-redux/selectors/entities/users';
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} from '@mm-redux/types/actions';

View File

@@ -5,7 +5,7 @@ import assert from 'assert';
import {Server, WebSocket as MockWebSocket} from 'mock-socket';
import {batchActions} from 'redux-batched-actions';
import {TeamTypes, UserTypes} from '@mm-redux/action_types';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import * as Actions from '@actions/websocket';
import {WebsocketEvents} from '@constants';

View File

@@ -6,7 +6,7 @@ import {loadChannelsForTeam} from '@actions/views/channel';
import {getMe} from '@actions/views/user';
import {ChannelTypes, TeamTypes, UserTypes, RoleTypes} from '@mm-redux/action_types';
import {notVisibleUsersActions} from '@mm-redux/actions/helpers';
import {Client4} from '@client/rest';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import {getAllChannels, getCurrentChannelId, getChannelMembersInChannels} from '@mm-redux/selectors/entities/channels';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
@@ -171,7 +171,7 @@ export function handleUserRoleUpdated(msg: WebSocketMessage) {
dispatch({
type: RoleTypes.RECEIVED_ROLES,
data,
data: data.roles,
});
} catch {
// do nothing

View File

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

View File

@@ -1,44 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {AppBinding, AppCallRequest, AppCallResponse, AppCallType} from '@mm-redux/types/apps';
import {buildQueryString} from '@mm-redux/utils/helpers';
export interface ClientAppsMix {
executeAppCall: (call: AppCallRequest, type: AppCallType) => Promise<AppCallResponse>;
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
}
const ClientApps = (superclass: any) => class extends superclass {
executeAppCall = async (call: AppCallRequest, type: AppCallType) => {
const callCopy = {
...call,
path: `${call.path}/${type}`,
context: {
...call.context,
user_agent: 'mobile',
},
};
return this.doFetch(
`${this.getAppsProxyRoute()}/api/v1/call`,
{method: 'post', body: JSON.stringify(callCopy)},
);
}
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
const params = {
user_id: userID,
channel_id: channelID,
team_id: teamID,
user_agent: 'mobile',
};
return this.doFetch(
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
{method: 'get'},
);
}
};
export default ClientApps;

View File

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

View File

@@ -1,39 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {buildQueryString} from '@mm-redux/utils/helpers';
import type {Bot} from '@mm-redux/types/bots';
export interface ClientBotsMix {
getBot: (botUserId: string) => Promise<Bot>;
getBots: (page?: number, perPage?: number) => Promise<Bot[]>;
getBotsIncludeDeleted: (page?: number, perPage?: number) => Promise<Bot[]>;
}
const PER_PAGE_DEFAULT = 60;
const ClientBots = (superclass: any) => class extends superclass {
getBot = async (botUserId: string) => {
return this.doFetch(
`${this.getBotRoute(botUserId)}`,
{method: 'get'},
);
}
getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
}
getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`,
{method: 'get'},
);
}
};
export default ClientBots;

View File

@@ -1,311 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {analytics} from '@init/analytics';
import {Channel, ChannelMemberCountByGroup, ChannelMembership, ChannelNotifyProps, ChannelStats} from '@mm-redux/types/channels';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientChannelsMix {
getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise<any>;
createChannel: (channel: Channel) => Promise<Channel>;
createDirectChannel: (userIds: string[]) => Promise<Channel>;
createGroupChannel: (userIds: string[]) => Promise<Channel>;
deleteChannel: (channelId: string) => Promise<any>;
unarchiveChannel: (channelId: string) => Promise<Channel>;
updateChannel: (channel: Channel) => Promise<Channel>;
convertChannelToPrivate: (channelId: string) => Promise<Channel>;
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => Promise<any>;
getChannel: (channelId: string) => Promise<Channel>;
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number) => Promise<Channel[]>;
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
getMyChannelMembers: (teamId: string) => Promise<ChannelMembership[]>;
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
getChannelTimezones: (channelId: string) => Promise<string[]>;
getChannelMember: (channelId: string, userId: string) => Promise<ChannelMembership>;
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
getChannelStats: (channelId: string) => Promise<ChannelStats>;
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
viewMyChannel: (channelId: string, prevChannelId?: string) => Promise<any>;
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
}
const ClientChannels = (superclass: any) => class extends superclass {
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
const queryData = {
page,
per_page: perPage,
not_associated_to_group: notAssociatedToGroup,
exclude_default_channels: excludeDefaultChannels,
include_total_count: includeTotalCount,
};
return this.doFetch(
`${this.getChannelsRoute()}${buildQueryString(queryData)}`,
{method: 'get'},
);
};
createChannel = async (channel: Channel) => {
analytics.trackAPI('api_channels_create', {team_id: channel.team_id});
return this.doFetch(
`${this.getChannelsRoute()}`,
{method: 'post', body: JSON.stringify(channel)},
);
};
createDirectChannel = async (userIds: string[]) => {
analytics.trackAPI('api_channels_create_direct');
return this.doFetch(
`${this.getChannelsRoute()}/direct`,
{method: 'post', body: JSON.stringify(userIds)},
);
};
createGroupChannel = async (userIds: string[]) => {
analytics.trackAPI('api_channels_create_group');
return this.doFetch(
`${this.getChannelsRoute()}/group`,
{method: 'post', body: JSON.stringify(userIds)},
);
};
deleteChannel = async (channelId: string) => {
analytics.trackAPI('api_channels_delete', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
{method: 'delete'},
);
};
unarchiveChannel = async (channelId: string) => {
analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/restore`,
{method: 'post'},
);
};
updateChannel = async (channel: Channel) => {
analytics.trackAPI('api_channels_update', {channel_id: channel.id});
return this.doFetch(
`${this.getChannelRoute(channel.id)}`,
{method: 'put', body: JSON.stringify(channel)},
);
};
convertChannelToPrivate = async (channelId: string) => {
analytics.trackAPI('api_channels_convert_to_private', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/convert`,
{method: 'post'},
);
};
updateChannelPrivacy = async (channelId: string, privacy: any) => {
analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
return this.doFetch(
`${this.getChannelRoute(channelId)}/privacy`,
{method: 'put', body: JSON.stringify({privacy})},
);
};
patchChannel = async (channelId: string, channelPatch: Partial<Channel>) => {
analytics.trackAPI('api_channels_patch', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/patch`,
{method: 'put', body: JSON.stringify(channelPatch)},
);
};
updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string, user_id: string}) => {
analytics.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
return this.doFetch(
`${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`,
{method: 'put', body: JSON.stringify(props)},
);
};
getChannel = async (channelId: string) => {
analytics.trackAPI('api_channel_get', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
{method: 'get'},
);
};
getChannelByName = async (teamId: string, channelName: string, includeDeleted = false) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
{method: 'get'},
);
};
getChannelByNameAndTeamName = async (teamName: string, channelName: string, includeDeleted = false) => {
analytics.trackAPI('api_channel_get_by_name_and_teamName', {channel_name: channelName, team_name: teamName, include_deleted: includeDeleted});
return this.doFetch(
`${this.getTeamNameRoute(teamName)}/channels/name/${channelName}?include_deleted=${includeDeleted}`,
{method: 'get'},
);
};
getChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
getArchivedChannels = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/deleted${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
getMyChannels = async (teamId: string, includeDeleted = false, lastDeleteAt = 0) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
include_deleted: includeDeleted,
last_delete_at: lastDeleteAt,
})}`,
{method: 'get'},
);
};
getMyChannelMember = async (channelId: string) => {
return this.doFetch(
`${this.getChannelMemberRoute(channelId, 'me')}`,
{method: 'get'},
);
};
getMyChannelMembers = async (teamId: string) => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/${teamId}/channels/members`,
{method: 'get'},
);
};
getChannelMembers = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
getChannelTimezones = async (channelId: string) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/timezones`,
{method: 'get'},
);
};
getChannelMember = async (channelId: string, userId: string) => {
return this.doFetch(
`${this.getChannelMemberRoute(channelId, userId)}`,
{method: 'get'},
);
};
getChannelMembersByIds = async (channelId: string, userIds: string[]) => {
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/ids`,
{method: 'post', body: JSON.stringify(userIds)},
);
};
addToChannel = async (userId: string, channelId: string, postRootId = '') => {
analytics.trackAPI('api_channels_add_member', {channel_id: channelId});
const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId};
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}`,
{method: 'post', body: JSON.stringify(member)},
);
};
removeFromChannel = async (userId: string, channelId: string) => {
analytics.trackAPI('api_channels_remove_member', {channel_id: channelId});
return this.doFetch(
`${this.getChannelMemberRoute(channelId, userId)}`,
{method: 'delete'},
);
};
getChannelStats = async (channelId: string) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/stats`,
{method: 'get'},
);
};
getChannelMemberCountsByGroup = async (channelId: string, includeTimezones: boolean) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/member_counts_by_group?include_timezones=${includeTimezones}`,
{method: 'get'},
);
};
viewMyChannel = async (channelId: string, prevChannelId?: string) => {
const data = {channel_id: channelId, prev_channel_id: prevChannelId};
return this.doFetch(
`${this.getChannelsRoute()}/members/me/view`,
{method: 'post', body: JSON.stringify(data)},
);
};
autocompleteChannels = async (teamId: string, name: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/autocomplete${buildQueryString({name})}`,
{method: 'get'},
);
};
autocompleteChannelsForSearch = async (teamId: string, name: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search_autocomplete${buildQueryString({name})}`,
{method: 'get'},
);
};
searchChannels = async (teamId: string, term: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search`,
{method: 'post', body: JSON.stringify({term})},
);
};
searchArchivedChannels = async (teamId: string, term: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search_archived`,
{method: 'post', body: JSON.stringify({term})},
);
};
};
export default ClientChannels;

View File

@@ -1,14 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const HEADER_AUTH = 'Authorization';
export const HEADER_BEARER = 'BEARER';
export const HEADER_REQUESTED_WITH = 'X-Requested-With';
export const HEADER_USER_AGENT = 'User-Agent';
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
export const HEADER_TOKEN = 'Token';
export const HEADER_X_VERSION_ID = 'X-Version-Id';
export const DEFAULT_LIMIT_BEFORE = 30;
export const DEFAULT_LIMIT_AFTER = 30;
export const PER_PAGE_DEFAULT = 60;

View File

@@ -1,101 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import FormData from 'form-data';
import {analytics} from '@init/analytics';
import {CustomEmoji} from '@mm-redux/types/emojis';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientEmojisMix {
createCustomEmoji: (emoji: CustomEmoji, imageData: any) => Promise<CustomEmoji>;
getCustomEmoji: (id: string) => Promise<CustomEmoji>;
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
getCustomEmojis: (page?: number, perPage?: number, sort?: string) => Promise<CustomEmoji[]>;
deleteCustomEmoji: (emojiId: string) => Promise<any>;
getSystemEmojiImageUrl: (filename: string) => string;
getCustomEmojiImageUrl: (id: string) => string;
searchCustomEmoji: (term: string, options?: Record<string, any>) => Promise<CustomEmoji[]>;
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
}
const ClientEmojis = (superclass: any) => class extends superclass {
createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => {
analytics.trackAPI('api_emoji_custom_add');
const formData = new FormData();
formData.append('image', imageData);
formData.append('emoji', JSON.stringify(emoji));
const request: any = {
method: 'post',
body: formData,
};
if (formData.getBoundary) {
request.headers = {
'Content-Type': `multipart/form-data; boundary=${formData.getBoundary()}`,
};
}
return this.doFetch(
`${this.getEmojisRoute()}`,
request,
);
};
getCustomEmoji = async (id: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/${id}`,
{method: 'get'},
);
};
getCustomEmojiByName = async (name: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/name/${name}`,
{method: 'get'},
);
};
getCustomEmojis = async (page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
return this.doFetch(
`${this.getEmojisRoute()}${buildQueryString({page, per_page: perPage, sort})}`,
{method: 'get'},
);
};
deleteCustomEmoji = async (emojiId: string) => {
analytics.trackAPI('api_emoji_custom_delete');
return this.doFetch(
`${this.getEmojiRoute(emojiId)}`,
{method: 'delete'},
);
};
getSystemEmojiImageUrl = (filename: string) => {
return `${this.url}/static/emoji/${filename}.png`;
};
getCustomEmojiImageUrl = (id: string) => {
return `${this.getEmojiRoute(id)}/image`;
};
searchCustomEmoji = async (term: string, options = {}) => {
return this.doFetch(
`${this.getEmojisRoute()}/search`,
{method: 'post', body: JSON.stringify({term, ...options})},
);
};
autocompleteCustomEmoji = async (name: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/autocomplete${buildQueryString({name})}`,
{method: 'get'},
);
};
};
export default ClientEmojis;

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {cleanUrlForLogging} from '@mm-redux/utils/sentry';
export default class ClientError extends Error {
url: string;
intl: { defaultMessage: string; id: string } | { defaultMessage: string; id: string } | { id: string; defaultMessage: string; values: any } | { id: string; defaultMessage: string };
server_error_id: any;
status_code: any;
details: Error;
constructor(baseUrl: string, data: any) {
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
this.message = data.message;
this.url = data.url;
this.intl = data.intl;
this.server_error_id = data.server_error_id;
this.status_code = data.status_code;
this.details = data.details;
// Ensure message is treated as a property of this class when object spreading. Without this,
// copying the object by using `{...error}` would not include the message.
Object.defineProperty(this, 'message', {enumerable: true});
}
}

View File

@@ -1,47 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export interface ClientFilesMix {
getFileUrl: (fileId: string, timestamp: number) => string;
getFileThumbnailUrl: (fileId: string, timestamp: number) => string;
getFilePreviewUrl: (fileId: string, timestamp: number) => string;
getFilePublicLink: (fileId: string) => Promise<any>;
}
const ClientFiles = (superclass: any) => class extends superclass {
getFileUrl(fileId: string, timestamp: number) {
let url = `${this.getFileRoute(fileId)}`;
if (timestamp) {
url += `?${timestamp}`;
}
return url;
}
getFileThumbnailUrl(fileId: string, timestamp: number) {
let url = `${this.getFileRoute(fileId)}/thumbnail`;
if (timestamp) {
url += `?${timestamp}`;
}
return url;
}
getFilePreviewUrl(fileId: string, timestamp: number) {
let url = `${this.getFileRoute(fileId)}/preview`;
if (timestamp) {
url += `?${timestamp}`;
}
return url;
}
getFilePublicLink = async (fileId: string) => {
return this.doFetch(
`${this.getFileRoute(fileId)}/link`,
{method: 'get'},
);
}
};
export default ClientFiles;

View File

@@ -1,98 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Config} from '@mm-redux/types/config';
import {Role} from '@mm-redux/types/roles';
import {Dictionary} from '@mm-redux/types/utilities';
import {buildQueryString} from '@mm-redux/utils/helpers';
import ClientError from './error';
export interface ClientGeneralMix {
getOpenGraphMetadata: (url: string) => Promise<any>;
ping: () => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: () => Promise<Config>;
getClientLicenseOld: () => Promise<any>;
getTimezones: () => Promise<string[]>;
getDataRetentionPolicy: () => Promise<any>;
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
getRedirectLocation: (urlParam: string) => Promise<Dictionary<string>>;
}
const ClientGeneral = (superclass: any) => class extends superclass {
getOpenGraphMetadata = async (url: string) => {
return this.doFetch(
`${this.getBaseRoute()}/opengraph`,
{method: 'post', body: JSON.stringify({url})},
);
};
ping = async () => {
return this.doFetch(
`${this.getBaseRoute()}/system/ping?time=${Date.now()}`,
{method: 'get'},
);
};
logClientError = async (message: string, level = 'ERROR') => {
const url = `${this.getBaseRoute()}/logs`;
if (!this.enableLogging) {
throw new ClientError(this.getUrl(), {
message: 'Logging disabled.',
url,
});
}
return this.doFetch(
url,
{method: 'post', body: JSON.stringify({message, level})},
);
};
getClientConfigOld = async () => {
return this.doFetch(
`${this.getBaseRoute()}/config/client?format=old`,
{method: 'get'},
);
};
getClientLicenseOld = async () => {
return this.doFetch(
`${this.getBaseRoute()}/license/client?format=old`,
{method: 'get'},
);
};
getTimezones = async () => {
return this.doFetch(
`${this.getTimezonesRoute()}`,
{method: 'get'},
);
};
getDataRetentionPolicy = () => {
return this.doFetch(
`${this.getDataRetentionRoute()}/policy`,
{method: 'get'},
);
};
getRolesByNames = async (rolesNames: string[]) => {
return this.doFetch(
`${this.getRolesRoute()}/names`,
{method: 'post', body: JSON.stringify(rolesNames)},
);
};
getRedirectLocation = async (urlParam: string) => {
if (!urlParam.length) {
return Promise.resolve();
}
const url = `${this.getRedirectLocationRoute()}${buildQueryString({url: urlParam})}`;
return this.doFetch(url, {method: 'get'});
};
};
export default ClientGeneral;

View File

@@ -1,53 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Group} from '@mm-redux/types/groups';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientGroupsMix {
getGroups: (filterAllowReference?: boolean, page?: number, perPage?: number) => Promise<Group[]>;
getGroupsByUserId: (userID: string) => Promise<Group[]>;
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
getAllGroupsAssociatedToChannelsInTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelID: string, filterAllowReference?: boolean) => Promise<Group[]>;
}
const ClientGroups = (superclass: any) => class extends superclass {
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT, since = 0) => {
return this.doFetch(
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
{method: 'get'},
);
};
getGroupsByUserId = async (userID: string) => {
return this.doFetch(
`${this.getUsersRoute()}/${userID}/groups`,
{method: 'get'},
);
}
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannelsInTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannel = async (channelID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
};
export default ClientGroups;

View File

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

View File

@@ -1,68 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {analytics} from '@init/analytics';
import {Command, DialogSubmission} from '@mm-redux/types/integrations';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientIntegrationsMix {
getCommandsList: (teamId: string) => Promise<Command[]>;
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, commandArgs?: Record<string, any>) => Promise<Command[]>;
getAutocompleteCommandsList: (teamId: string, page?: number, perPage?: number) => Promise<Command[]>;
executeCommand: (command: Command, commandArgs?: Record<string, any>) => Promise<any>;
addCommand: (command: Command) => Promise<Command>;
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
}
const ClientIntegrations = (superclass: any) => class extends superclass {
getCommandsList = async (teamId: string) => {
return this.doFetch(
`${this.getCommandsRoute()}?team_id=${teamId}`,
{method: 'get'},
);
};
getCommandAutocompleteSuggestionsList = async (userInput: string, teamId: string, commandArgs: {}) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/commands/autocomplete_suggestions${buildQueryString({...commandArgs, user_input: userInput})}`,
{method: 'get'},
);
};
getAutocompleteCommandsList = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/commands/autocomplete${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
executeCommand = async (command: Command, commandArgs = {}) => {
analytics.trackAPI('api_integrations_used');
return this.doFetch(
`${this.getCommandsRoute()}/execute`,
{method: 'post', body: JSON.stringify({command, ...commandArgs})},
);
};
addCommand = async (command: Command) => {
analytics.trackAPI('api_integrations_created');
return this.doFetch(
`${this.getCommandsRoute()}`,
{method: 'post', body: JSON.stringify(command)},
);
};
submitInteractiveDialog = async (data: DialogSubmission) => {
analytics.trackAPI('api_interactive_messages_dialog_submitted');
return this.doFetch(
`${this.getBaseRoute()}/actions/dialogs/submit`,
{method: 'post', body: JSON.stringify(data)},
);
};
};
export default ClientIntegrations;

View File

@@ -1,237 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {analytics} from '@init/analytics';
import {FileInfo} from '@mm-redux/types/files';
import {Post} from '@mm-redux/types/posts';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientPostsMix {
createPost: (post: Post) => Promise<Post>;
updatePost: (post: Post) => Promise<Post>;
getPost: (postId: string) => Promise<Post>;
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
deletePost: (postId: string) => Promise<any>;
getPostThread: (postId: string) => Promise<any>;
getPosts: (channelId: string, page?: number, perPage?: number) => Promise<any>;
getPostsSince: (channelId: string, since: number) => Promise<any>;
getPostsBefore: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number) => Promise<any>;
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
getFlaggedPosts: (userId: string, channelId?: string, teamId?: string, page?: number, perPage?: number) => Promise<any>;
getPinnedPosts: (channelId: string) => Promise<any>;
markPostAsUnread: (userId: string, postId: string) => Promise<any>;
pinPost: (postId: string) => Promise<any>;
unpinPost: (postId: string) => Promise<any>;
addReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
removeReaction: (userId: string, postId: string, emojiName: string) => Promise<any>;
getReactionsForPost: (postId: string) => Promise<any>;
searchPostsWithParams: (teamId: string, params: any) => Promise<any>;
searchPosts: (teamId: string, terms: string, isOrSearch: boolean) => Promise<any>;
doPostAction: (postId: string, actionId: string, selectedOption?: string) => Promise<any>;
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
}
const ClientPosts = (superclass: any) => class extends superclass {
createPost = async (post: Post) => {
analytics.trackAPI('api_posts_create', {channel_id: post.channel_id});
if (post.root_id != null && post.root_id !== '') {
analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id});
}
return this.doFetch(
`${this.getPostsRoute()}`,
{method: 'post', body: JSON.stringify(post)},
);
};
updatePost = async (post: Post) => {
analytics.trackAPI('api_posts_update', {channel_id: post.channel_id});
return this.doFetch(
`${this.getPostRoute(post.id)}`,
{method: 'put', body: JSON.stringify(post)},
);
};
getPost = async (postId: string) => {
return this.doFetch(
`${this.getPostRoute(postId)}`,
{method: 'get'},
);
};
patchPost = async (postPatch: Partial<Post> & {id: string}) => {
analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
return this.doFetch(
`${this.getPostRoute(postPatch.id)}/patch`,
{method: 'put', body: JSON.stringify(postPatch)},
);
};
deletePost = async (postId: string) => {
analytics.trackAPI('api_posts_delete');
return this.doFetch(
`${this.getPostRoute(postId)}`,
{method: 'delete'},
);
};
getPostThread = async (postId: string) => {
return this.doFetch(
`${this.getPostRoute(postId)}/thread`,
{method: 'get'},
);
};
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
getPostsSince = async (channelId: string, since: number) => {
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({since})}`,
{method: 'get'},
);
};
getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
analytics.trackAPI('api_posts_get_before', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage})}`,
{method: 'get'},
);
};
getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
analytics.trackAPI('api_posts_get_after', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage})}`,
{method: 'get'},
);
};
getFileInfosForPost = async (postId: string) => {
return this.doFetch(
`${this.getPostRoute(postId)}/files/info`,
{method: 'get'},
);
};
getFlaggedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
analytics.trackAPI('api_posts_get_flagged', {team_id: teamId});
return this.doFetch(
`${this.getUserRoute(userId)}/posts/flagged${buildQueryString({channel_id: channelId, team_id: teamId, page, per_page: perPage})}`,
{method: 'get'},
);
};
getPinnedPosts = async (channelId: string) => {
analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/pinned`,
{method: 'get'},
);
};
markPostAsUnread = async (userId: string, postId: string) => {
analytics.trackAPI('api_post_set_unread_post');
return this.doFetch(
`${this.getUserRoute(userId)}/posts/${postId}/set_unread`,
{method: 'post'},
);
}
pinPost = async (postId: string) => {
analytics.trackAPI('api_posts_pin');
return this.doFetch(
`${this.getPostRoute(postId)}/pin`,
{method: 'post'},
);
};
unpinPost = async (postId: string) => {
analytics.trackAPI('api_posts_unpin');
return this.doFetch(
`${this.getPostRoute(postId)}/unpin`,
{method: 'post'},
);
};
addReaction = async (userId: string, postId: string, emojiName: string) => {
analytics.trackAPI('api_reactions_save', {post_id: postId});
return this.doFetch(
`${this.getReactionsRoute()}`,
{method: 'post', body: JSON.stringify({user_id: userId, post_id: postId, emoji_name: emojiName})},
);
};
removeReaction = async (userId: string, postId: string, emojiName: string) => {
analytics.trackAPI('api_reactions_delete', {post_id: postId});
return this.doFetch(
`${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`,
{method: 'delete'},
);
};
getReactionsForPost = async (postId: string) => {
return this.doFetch(
`${this.getPostRoute(postId)}/reactions`,
{method: 'get'},
);
};
searchPostsWithParams = async (teamId: string, params: any) => {
analytics.trackAPI('api_posts_search', {team_id: teamId});
return this.doFetch(
`${this.getTeamRoute(teamId)}/posts/search`,
{method: 'post', body: JSON.stringify(params)},
);
};
searchPosts = async (teamId: string, terms: string, isOrSearch: boolean) => {
return this.searchPostsWithParams(teamId, {terms, is_or_search: isOrSearch});
};
doPostAction = async (postId: string, actionId: string, selectedOption = '') => {
return this.doPostActionWithCookie(postId, actionId, '', selectedOption);
};
doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
if (selectedOption) {
analytics.trackAPI('api_interactive_messages_menu_selected');
} else {
analytics.trackAPI('api_interactive_messages_button_clicked');
}
const msg: any = {
selected_option: selectedOption,
};
if (actionCookie !== '') {
msg.cookie = actionCookie;
}
return this.doFetch(
`${this.getPostRoute(postId)}/actions/${encodeURIComponent(actionId)}`,
{method: 'post', body: JSON.stringify(msg)},
);
};
};
export default ClientPosts;

View File

@@ -1,35 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PreferenceType} from '@mm-redux/types/preferences';
export interface ClientPreferencesMix {
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: () => Promise<PreferenceType>;
}
const ClientPreferences = (superclass: any) => class extends superclass {
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
return this.doFetch(
`${this.getPreferencesRoute(userId)}`,
{method: 'put', body: JSON.stringify(preferences)},
);
};
getMyPreferences = async () => {
return this.doFetch(
`${this.getPreferencesRoute('me')}`,
{method: 'get'},
);
};
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
return this.doFetch(
`${this.getPreferencesRoute(userId)}/delete`,
{method: 'post', body: JSON.stringify(preferences)},
);
};
};
export default ClientPreferences;

View File

@@ -1,177 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {analytics} from '@init/analytics';
import {Team, TeamMembership, TeamUnread} from '@mm-redux/types/teams';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
export interface ClientTeamsMix {
createTeam: (team: Team) => Promise<Team>;
deleteTeam: (teamId: string) => Promise<any>;
updateTeam: (team: Team) => Promise<Team>;
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<any>;
getTeam: (teamId: string) => Promise<Team>;
getTeamByName: (teamName: string) => Promise<Team>;
getMyTeams: () => Promise<Team[]>;
getTeamsForUser: (userId: string) => Promise<Team[]>;
getMyTeamMembers: () => Promise<TeamMembership[]>;
getMyTeamUnreads: () => Promise<TeamUnread[]>;
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
getTeamMember: (teamId: string, userId: string) => Promise<TeamMembership>;
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
joinTeam: (inviteId: string) => Promise<TeamMembership>;
removeFromTeam: (teamId: string, userId: string) => Promise<any>;
getTeamStats: (teamId: string) => Promise<any>;
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
}
const ClientTeams = (superclass: any) => class extends superclass {
createTeam = async (team: Team) => {
analytics.trackAPI('api_teams_create');
return this.doFetch(
`${this.getTeamsRoute()}`,
{method: 'post', body: JSON.stringify(team)},
);
};
deleteTeam = async (teamId: string) => {
analytics.trackAPI('api_teams_delete');
return this.doFetch(
`${this.getTeamRoute(teamId)}`,
{method: 'delete'},
);
};
updateTeam = async (team: Team) => {
analytics.trackAPI('api_teams_update_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}`,
{method: 'put', body: JSON.stringify(team)},
);
};
patchTeam = async (team: Partial<Team> & {id: string}) => {
analytics.trackAPI('api_teams_patch_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}/patch`,
{method: 'put', body: JSON.stringify(team)},
);
};
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => {
return this.doFetch(
`${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`,
{method: 'get'},
);
};
getTeam = async (teamId: string) => {
return this.doFetch(
this.getTeamRoute(teamId),
{method: 'get'},
);
};
getTeamByName = async (teamName: string) => {
analytics.trackAPI('api_teams_get_team_by_name');
return this.doFetch(
this.getTeamNameRoute(teamName),
{method: 'get'},
);
};
getMyTeams = async () => {
return this.doFetch(
`${this.getUserRoute('me')}/teams`,
{method: 'get'},
);
};
getTeamsForUser = async (userId: string) => {
return this.doFetch(
`${this.getUserRoute(userId)}/teams`,
{method: 'get'},
);
};
getMyTeamMembers = async () => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/members`,
{method: 'get'},
);
};
getMyTeamUnreads = async () => {
return this.doFetch(
`${this.getUserRoute('me')}/teams/unread`,
{method: 'get'},
);
};
getTeamMembers = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getTeamMembersRoute(teamId)}${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
};
getTeamMember = async (teamId: string, userId: string) => {
return this.doFetch(
`${this.getTeamMemberRoute(teamId, userId)}`,
{method: 'get'},
);
};
addToTeam = async (teamId: string, userId: string) => {
analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
const member = {user_id: userId, team_id: teamId};
return this.doFetch(
`${this.getTeamMembersRoute(teamId)}`,
{method: 'post', body: JSON.stringify(member)},
);
};
joinTeam = async (inviteId: string) => {
const query = buildQueryString({invite_id: inviteId});
return this.doFetch(
`${this.getTeamsRoute()}/members/invite${query}`,
{method: 'post'},
);
};
removeFromTeam = async (teamId: string, userId: string) => {
analytics.trackAPI('api_teams_remove_members', {team_id: teamId});
return this.doFetch(
`${this.getTeamMemberRoute(teamId, userId)}`,
{method: 'delete'},
);
};
getTeamStats = async (teamId: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/stats`,
{method: 'get'},
);
};
getTeamIconUrl = (teamId: string, lastTeamIconUpdate: number) => {
const params: any = {};
if (lastTeamIconUpdate) {
params._ = lastTeamIconUpdate;
}
return `${this.getTeamRoute(teamId)}/image${buildQueryString(params)}`;
};
};
export default ClientTeams;

View File

@@ -1,25 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export interface ClientTosMix {
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<any>;
getTermsOfService: () => Promise<any>;
}
const ClientTos = (superclass: any) => class extends superclass {
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
return this.doFetch(
`${this.getUserRoute('me')}/terms_of_service`,
{method: 'post', body: JSON.stringify({termsOfServiceId, accepted})},
);
}
getTermsOfService = async () => {
return this.doFetch(
`${this.getBaseRoute()}/terms_of_service`,
{method: 'get'},
);
}
};
export default ClientTos;

View File

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

View File

@@ -2,14 +2,16 @@
exports[`AtMention should match snapshot, no highlight 1`] = `
<Text
style={
Object {
"backgroundColor": "yellow",
}
}
style={Object {}}
>
<Text
style={Array []}
style={
Array [
Object {
"backgroundColor": "yellow",
},
]
}
>
@John.Smith
</Text>
@@ -20,11 +22,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
<Text
onLongPress={[Function]}
onPress={[Function]}
style={
Object {
"backgroundColor": "yellow",
}
}
style={Object {}}
>
<Text
style={
@@ -33,8 +31,7 @@ exports[`AtMention should match snapshot, with highlight 1`] = `
"color": "#ff0000",
},
Object {
"backgroundColor": "#ffe577",
"color": "#166de0",
"backgroundColor": "yellow",
},
]
}
@@ -48,11 +45,7 @@ exports[`AtMention should match snapshot, without highlight 1`] = `
<Text
onLongPress={[Function]}
onPress={[Function]}
style={
Object {
"backgroundColor": "yellow",
}
}
style={Object {}}
>
<Text
style={

View File

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

View File

@@ -3,9 +3,6 @@
import React from 'react';
import {shallow} from 'enzyme';
import {Preferences} from '@mm-redux/constants';
import AtMention from './at_mention.js';
describe('AtMention', () => {
@@ -16,7 +13,7 @@ describe('AtMention', () => {
mentionName: 'John.Smith',
mentionStyle: {color: '#ff0000'},
textStyle: {backgroundColor: 'yellow'},
theme: Preferences.THEMES.default,
theme: {},
};
test('should match snapshot, no highlight', () => {

View File

@@ -3,17 +3,17 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, SectionList} from 'react-native';
import {SectionList} from 'react-native';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from '@constants/autocomplete';
import AtMentionItem from '@components/autocomplete/at_mention_item';
import AutocompleteSectionHeader from '@components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from '@components/autocomplete/special_mention_item';
import GroupMentionItem from '@components/autocomplete/at_mention_group/at_mention_group';
import {RequestStatus} from '@mm-redux/constants';
import {debounce} from '@mm-redux/actions/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {t} from '@utils/i18n';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import GroupMentionItem from 'app/components/autocomplete/at_mention_group/at_mention_group';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
export default class AtMention extends PureComponent {
static propTypes = {
@@ -54,15 +54,9 @@ export default class AtMention extends PureComponent {
sections: [],
};
}
runSearch = debounce((currentTeamId, channelId, matchTerm) => {
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
}, 200);
updateSections(sections) {
this.setState({sections});
}
componentDidUpdate(prevProps, prevState) {
if (this.props.matchTerm !== prevProps.matchTerm) {
if (this.props.matchTerm === null) {
@@ -74,9 +68,9 @@ export default class AtMention extends PureComponent {
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
// Update user autocomplete list with results of server request
const {currentTeamId, currentChannelId, matchTerm} = this.props;
const {currentTeamId, currentChannelId} = this.props;
const channelId = this.props.isSearch ? '' : currentChannelId;
this.runSearch(currentTeamId, channelId, matchTerm);
this.props.actions.autocompleteUsers(this.props.matchTerm, currentTeamId, channelId);
}
}
if (this.props.matchTerm !== null && this.props.matchTerm === prevProps.matchTerm) {
@@ -257,16 +251,15 @@ export default class AtMention extends PureComponent {
return (
<SectionList
testID='at_mention_suggestion.list'
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
removeClippedSubviews={Platform.OS === 'android'}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
style={[style.listView, {maxHeight: maxListHeight}]}
sections={sections}
testID='at_mention_suggestion.list'
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
/>
);
}

View File

@@ -115,7 +115,6 @@ const AtMentionItem = (props: AtMentionItemProps) => {
size={24}
status={null}
showStatus={false}
testID={`${testID}.profile_picture`}
/>
</View>
<BotTag

View File

@@ -17,6 +17,7 @@ import {t} from 'app/utils/i18n';
export default class ChannelMention extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired,
autocompleteChannelsForSearch: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
@@ -205,7 +206,6 @@ export default class ChannelMention extends PureComponent {
<ChannelMentionItem
channelId={item}
onPress={this.completeMention}
testID={`autocomplete.channel_mention.item.${item}`}
/>
);
};
@@ -224,16 +224,15 @@ export default class ChannelMention extends PureComponent {
return (
<SectionList
testID='channel_mention_suggestion.list'
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
removeClippedSubviews={Platform.OS === 'android'}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
style={[style.listView, {maxHeight: maxListHeight}]}
sections={sections}
testID='channel_mention_suggestion.list'
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
initialNumToRender={10}
nestedScrollEnabled={nestedScrollEnabled}
/>
);
}

View File

@@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
import {searchChannels, autocompleteChannelsForSearch} from '@mm-redux/actions/channels';
import {getMyChannelMemberships} from '@mm-redux/selectors/entities/channels';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
@@ -56,6 +56,7 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
searchChannels,
autocompleteChannelsForSearch,
}, dispatch),
};

View File

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

View File

@@ -200,7 +200,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"bust_in_silhouette",
"busts_in_silhouette",
"butterfly",
"ca",
"cactus",
"cake",
"calendar",
@@ -1024,7 +1023,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"pisces",
"pitcairn_islands",
"pizza",
"pk",
"place_of_worship",
"plate_with_cutlery",
"play_or_pause_button",
@@ -1507,7 +1505,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"yen",
"yin_yang",
"yum",
"za",
"zambia",
"zap",
"zero",
@@ -1715,7 +1712,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"bust_in_silhouette",
"busts_in_silhouette",
"butterfly",
"ca",
"cactus",
"cake",
"calendar",
@@ -2539,7 +2535,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"pisces",
"pitcairn_islands",
"pizza",
"pk",
"place_of_worship",
"plate_with_cutlery",
"play_or_pause_button",
@@ -3022,7 +3017,6 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
"yen",
"yin_yang",
"yum",
"za",
"zambia",
"zap",
"zero",
@@ -3042,7 +3036,7 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
numColumns={1}
onEndReachedThreshold={2}
pageSize={10}
removeClippedSubviews={true}
removeClippedSubviews={false}
renderItem={[Function]}
scrollEventThrottle={50}
style={

View File

@@ -230,7 +230,6 @@ export default class EmojiSuggestion extends PureComponent {
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
removeClippedSubviews={true}
renderItem={this.renderItem}
pageSize={10}
initialListSize={10}

View File

@@ -1,60 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
<FlatList
data={
Array [
Object {
"Complete": "thetrigger",
"Description": "The Description",
"Hint": "The Hint",
"IconData": "iconurl.com",
"Suggestion": "/thetrigger",
},
]
}
disableVirtualization={false}
extraData={
Object {
"active": true,
"dataSource": Array [
Object {
"Complete": "thetrigger",
"Description": "The Description",
"Hint": "The Hint",
"IconData": "iconurl.com",
"Suggestion": "/thetrigger",
},
],
"lastCommandRequest": 1234,
}
}
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
nestedScrollEnabled={false}
numColumns={1}
onEndReachedThreshold={2}
removeClippedSubviews={true}
renderItem={[Function]}
scrollEventThrottle={50}
style={
Array [
Object {
"backgroundColor": "#ffffff",
"borderRadius": 4,
"flex": 1,
"paddingTop": 8,
},
Object {
"maxHeight": 50,
},
]
}
testID="slash_suggestion.list"
updateCellsBatchingPeriod={50}
windowSize={21}
/>
`;

View File

@@ -1,953 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import {
thunk,
configureStore,
Client4,
AppBinding,
checkForExecuteSuggestion,
} from './tests/app_command_parser_test_dependencies';
import {
AppCallResponseTypes,
AppCallTypes,
AutocompleteSuggestion,
} from './app_command_parser_dependencies';
import {
AppCommandParser,
ParseState,
ParsedCommand,
} from './app_command_parser';
import {
reduxTestState,
testBindings,
} from './tests/app_command_parser_test_data';
const mockStore = configureStore([thunk]);
describe('AppCommandParser', () => {
const makeStore = async (bindings: AppBinding[]) => {
const initialState = {
...reduxTestState,
entities: {
...reduxTestState.entities,
apps: {bindings},
},
} as any;
const testStore = await mockStore(initialState);
return testStore;
};
const intl = {
formatMessage: (message: {id: string, defaultMessage: string}) => {
return message.defaultMessage;
},
};
let parser: AppCommandParser;
beforeEach(async () => {
const store = await makeStore(testBindings);
parser = new AppCommandParser(store as any, intl, 'current_channel_id', 'root_id');
});
type Variant = {
expectError?: string;
verify?(parsed: ParsedCommand): void;
}
type TC = {
title: string;
command: string;
submit: Variant;
autocomplete?: Variant; // if undefined, use same checks as submnit
}
const checkResult = (parsed: ParsedCommand, v: Variant) => {
if (v.expectError) {
expect(parsed.state).toBe(ParseState.Error);
expect(parsed.error).toBe(v.expectError);
} else {
// expect(parsed).toBe(1);
expect(parsed.error).toBe('');
expect(v.verify).toBeTruthy();
if (v.verify) {
v.verify(parsed);
}
}
};
describe('getSuggestionsBase', () => {
test('string matches 1', () => {
const res = parser.getSuggestionsBase('/');
expect(res).toHaveLength(2);
});
test('string matches 2', () => {
const res = parser.getSuggestionsBase('/ji');
expect(res).toHaveLength(1);
});
test('string matches 3', () => {
const res = parser.getSuggestionsBase('/jira');
expect(res).toHaveLength(1);
});
test('string matches case insensitive', () => {
const res = parser.getSuggestionsBase('/JiRa');
expect(res).toHaveLength(1);
});
test('string is past base command', () => {
const res = parser.getSuggestionsBase('/jira ');
expect(res).toHaveLength(0);
});
test('other command matches', () => {
const res = parser.getSuggestionsBase('/other');
expect(res).toHaveLength(1);
});
test('string does not match', () => {
const res = parser.getSuggestionsBase('/wrong');
expect(res).toHaveLength(0);
});
});
describe('matchBinding', () => {
const table: TC[] = [
{
title: 'full command',
command: '/jira issue create --project P --summary = "SUM MA RY" --verbose --epic=epic2',
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndCommand);
expect(parsed.binding?.label).toBe('create');
expect(parsed.incomplete).toBe('--project');
expect(parsed.incompleteStart).toBe(19);
}},
},
{
title: 'full command case insensitive',
command: '/JiRa IsSuE CrEaTe --PrOjEcT P --SuMmArY = "SUM MA RY" --VeRbOsE --EpIc=epic2',
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndCommand);
expect(parsed.binding?.label).toBe('create');
expect(parsed.incomplete).toBe('--PrOjEcT');
expect(parsed.incompleteStart).toBe(19);
}},
},
{
title: 'incomplete top command',
command: '/jir',
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
submit: {expectError: '`{command}`: No matching command found in this workspace.'},
},
{
title: 'no space after the top command',
command: '/jira',
autocomplete: {expectError: '`{command}`: No matching command found in this workspace.'},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('jira');
}},
},
{
title: 'space after the top command',
command: '/jira ',
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('jira');
}},
},
{
title: 'middle of subcommand',
command: '/jira iss',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('jira');
expect(parsed.incomplete).toBe('iss');
expect(parsed.incompleteStart).toBe(9);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndCommand);
expect(parsed.binding?.label).toBe('jira');
expect(parsed.incomplete).toBe('iss');
expect(parsed.incompleteStart).toBe(9);
}},
},
{
title: 'second subcommand, no space',
command: '/jira issue',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('jira');
expect(parsed.incomplete).toBe('issue');
expect(parsed.incompleteStart).toBe(6);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('issue');
expect(parsed.location).toBe('/jira/issue');
}},
},
{
title: 'token after the end of bindings, no space',
command: '/jira issue create something',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.Command);
expect(parsed.binding?.label).toBe('create');
expect(parsed.incomplete).toBe('something');
expect(parsed.incompleteStart).toBe(20);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndCommand);
expect(parsed.binding?.label).toBe('create');
expect(parsed.incomplete).toBe('something');
expect(parsed.incompleteStart).toBe(20);
}},
},
{
title: 'token after the end of bindings, with space',
command: '/jira issue create something ',
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndCommand);
expect(parsed.binding?.label).toBe('create');
expect(parsed.incomplete).toBe('something');
expect(parsed.incompleteStart).toBe(20);
}},
},
];
table.forEach((tc) => {
test(tc.title, async () => {
const bindings = testBindings[0].bindings as AppBinding[];
let a = new ParsedCommand(tc.command, parser, intl);
a = await a.matchBinding(bindings, true);
checkResult(a, tc.autocomplete || tc.submit);
let s = new ParsedCommand(tc.command, parser, intl);
s = await s.matchBinding(bindings, false);
checkResult(s, tc.submit);
});
});
});
describe('parseForm', () => {
const table: TC[] = [
{
title: 'happy full create',
command: '/jira issue create --project `P 1` --summary "SUM MA RY" --verbose --epic=epic2',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.incomplete).toBe('epic2');
expect(parsed.incompleteStart).toBe(75);
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.epic).toBeUndefined();
expect(parsed.values?.summary).toBe('SUM MA RY');
expect(parsed.values?.verbose).toBe('true');
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.epic).toBe('epic2');
expect(parsed.values?.summary).toBe('SUM MA RY');
expect(parsed.values?.verbose).toBe('true');
}},
},
{
title: 'happy full create case insensitive',
command: '/JiRa IsSuE CrEaTe --PrOjEcT `P 1` --SuMmArY "SUM MA RY" --VeRbOsE --EpIc=epic2',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.incomplete).toBe('epic2');
expect(parsed.incompleteStart).toBe(75);
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.epic).toBeUndefined();
expect(parsed.values?.summary).toBe('SUM MA RY');
expect(parsed.values?.verbose).toBe('true');
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.epic).toBe('epic2');
expect(parsed.values?.summary).toBe('SUM MA RY');
expect(parsed.values?.verbose).toBe('true');
}},
},
{
title: 'partial epic',
command: '/jira issue create --project KT --summary "great feature" --epic M',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.incomplete).toBe('M');
expect(parsed.incompleteStart).toBe(65);
expect(parsed.values?.project).toBe('KT');
expect(parsed.values?.epic).toBeUndefined();
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.values?.epic).toBe('M');
}},
},
{
title: 'happy full view',
command: '/jira issue view --project=`P 1` MM-123',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.incomplete).toBe('MM-123');
expect(parsed.incompleteStart).toBe(33);
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.issue).toBe(undefined);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.issue).toBe('MM-123');
}},
},
{
title: 'happy view no parameters',
command: '/jira issue view ',
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.StartParameter);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.incomplete).toBe('');
expect(parsed.incompleteStart).toBe(17);
expect(parsed.values).toEqual({});
}},
},
{
title: 'happy create flag no value',
command: '/jira issue create --summary ',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.FlagValueSeparator);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.incomplete).toBe('');
expect(parsed.values).toEqual({});
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndValue);
expect(parsed.binding?.label).toBe('create');
expect(parsed.form?.call?.path).toBe('/create-issue');
expect(parsed.incomplete).toBe('');
expect(parsed.values).toEqual({
summary: '',
});
}},
},
{
title: 'error: unmatched tick',
command: '/jira issue view --project `P 1',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.TickValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.incomplete).toBe('P 1');
expect(parsed.incompleteStart).toBe(27);
expect(parsed.values?.project).toBe(undefined);
expect(parsed.values?.issue).toBe(undefined);
}},
submit: {expectError: 'Matching tick quote expected before end of input.'},
},
{
title: 'error: unmatched quote',
command: '/jira issue view --project "P \\1',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.QuotedValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.incomplete).toBe('P 1');
expect(parsed.incompleteStart).toBe(27);
expect(parsed.values?.project).toBe(undefined);
expect(parsed.values?.issue).toBe(undefined);
}},
submit: {expectError: 'Matching double quote expected before end of input.'},
},
{
title: 'missing required fields not a problem for parseCommand',
command: '/jira issue view --project "P 1"',
autocomplete: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndQuotedValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.incomplete).toBe('P 1');
expect(parsed.incompleteStart).toBe(27);
expect(parsed.values?.project).toBe(undefined);
expect(parsed.values?.issue).toBe(undefined);
}},
submit: {verify: (parsed: ParsedCommand): void => {
expect(parsed.state).toBe(ParseState.EndQuotedValue);
expect(parsed.binding?.label).toBe('view');
expect(parsed.form?.call?.path).toBe('/view-issue');
expect(parsed.values?.project).toBe('P 1');
expect(parsed.values?.issue).toBe(undefined);
}},
},
{
title: 'error: invalid flag',
command: '/jira issue view --wrong test',
submit: {expectError: 'Command does not accept flag `{flagName}`.'},
},
{
title: 'error: unexpected positional',
command: '/jira issue create wrong',
submit: {expectError: 'Unable to identify argument.'},
},
{
title: 'error: multiple equal signs',
command: '/jira issue create --project == test',
submit: {expectError: 'Multiple `=` signs are not allowed.'},
},
];
table.forEach((tc) => {
test(tc.title, async () => {
const bindings = testBindings[0].bindings as AppBinding[];
let a = new ParsedCommand(tc.command, parser, intl);
a = await a.matchBinding(bindings, true);
a = a.parseForm(true);
checkResult(a, tc.autocomplete || tc.submit);
let s = new ParsedCommand(tc.command, parser, intl);
s = await s.matchBinding(bindings, false);
s = s.parseForm(false);
checkResult(s, tc.submit);
});
});
});
describe('getSuggestions', () => {
test('subcommand 1', async () => {
const suggestions = await parser.getSuggestions('/jira ');
expect(suggestions).toEqual([
{
Suggestion: 'issue',
Complete: 'jira issue',
Hint: 'Issue hint',
IconData: 'Issue icon',
Description: 'Interact with Jira issues',
},
]);
});
test('subcommand 1 case insensitive', async () => {
const suggestions = await parser.getSuggestions('/JiRa ');
expect(suggestions).toEqual([
{
Suggestion: 'issue',
Complete: 'JiRa issue',
Hint: 'Issue hint',
IconData: 'Issue icon',
Description: 'Interact with Jira issues',
},
]);
});
test('subcommand 2', async () => {
const suggestions = await parser.getSuggestions('/jira issue');
expect(suggestions).toEqual([
{
Suggestion: 'issue',
Complete: 'jira issue',
Hint: 'Issue hint',
IconData: 'Issue icon',
Description: 'Interact with Jira issues',
},
]);
});
test('subcommand 2 case insensitive', async () => {
const suggestions = await parser.getSuggestions('/JiRa IsSuE');
expect(suggestions).toEqual([
{
Suggestion: 'issue',
Complete: 'JiRa issue',
Hint: 'Issue hint',
IconData: 'Issue icon',
Description: 'Interact with Jira issues',
},
]);
});
test('subcommand 2 with a space', async () => {
const suggestions = await parser.getSuggestions('/jira issue ');
expect(suggestions).toEqual([
{
Suggestion: 'view',
Complete: 'jira issue view',
Hint: '',
IconData: '',
Description: 'View details of a Jira issue',
},
{
Suggestion: 'create',
Complete: 'jira issue create',
Hint: 'Create hint',
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
]);
});
test('subcommand 2 with a space case insensitive', async () => {
const suggestions = await parser.getSuggestions('/JiRa IsSuE ');
expect(suggestions).toEqual([
{
Suggestion: 'view',
Complete: 'JiRa IsSuE view',
Hint: '',
IconData: '',
Description: 'View details of a Jira issue',
},
{
Suggestion: 'create',
Complete: 'JiRa IsSuE create',
Hint: 'Create hint',
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
]);
});
test('subcommand 3 partial', async () => {
const suggestions = await parser.getSuggestions('/jira issue c');
expect(suggestions).toEqual([
{
Suggestion: 'create',
Complete: 'jira issue create',
Hint: 'Create hint',
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
]);
});
test('subcommand 3 partial case insensitive', async () => {
const suggestions = await parser.getSuggestions('/JiRa IsSuE C');
expect(suggestions).toEqual([
{
Suggestion: 'create',
Complete: 'JiRa IsSuE create',
Hint: 'Create hint',
IconData: 'Create icon',
Description: 'Create a new Jira issue',
},
]);
});
test('view just after subcommand (positional)', async () => {
const suggestions = await parser.getSuggestions('/jira issue view ');
expect(suggestions).toEqual([
{
Complete: 'jira issue view',
Description: 'The Jira issue key',
Hint: '',
IconData: '',
Suggestion: 'issue: ""',
},
]);
});
test('view flags just after subcommand', async () => {
let suggestions = await parser.getSuggestions('/jira issue view -');
expect(suggestions).toEqual([
{
Complete: 'jira issue view --project',
Description: 'The Jira project description',
Hint: 'The Jira project hint',
IconData: '',
Suggestion: '--project',
},
]);
suggestions = await parser.getSuggestions('/jira issue view --');
expect(suggestions).toEqual([
{
Complete: 'jira issue view --project',
Description: 'The Jira project description',
Hint: 'The Jira project hint',
IconData: '',
Suggestion: '--project',
},
]);
});
test('create flags just after subcommand', async () => {
const suggestions = await parser.getSuggestions('/jira issue create ');
let executeCommand: AutocompleteSuggestion[] = [];
if (checkForExecuteSuggestion) {
executeCommand = [
{
Complete: 'jira issue create _execute_current_command',
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
Hint: '',
IconData: '_execute_current_command',
Suggestion: 'Execute Current Command',
},
];
}
expect(suggestions).toEqual([
...executeCommand,
{
Complete: 'jira issue create --project',
Description: 'The Jira project description',
Hint: 'The Jira project hint',
IconData: 'Create icon',
Suggestion: '--project',
},
{
Complete: 'jira issue create --summary',
Description: 'The Jira issue summary',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--summary',
},
{
Complete: 'jira issue create --verbose',
Description: 'display details',
Hint: 'yes or no!',
IconData: 'Create icon',
Suggestion: '--verbose',
},
{
Complete: 'jira issue create --epic',
Description: 'The Jira epic',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--epic',
},
]);
});
test('used flags do not appear', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT ');
let executeCommand: AutocompleteSuggestion[] = [];
if (checkForExecuteSuggestion) {
executeCommand = [
{
Complete: 'jira issue create --project KT _execute_current_command',
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
Hint: '',
IconData: '_execute_current_command',
Suggestion: 'Execute Current Command',
},
];
}
expect(suggestions).toEqual([
...executeCommand,
{
Complete: 'jira issue create --project KT --summary',
Description: 'The Jira issue summary',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--summary',
},
{
Complete: 'jira issue create --project KT --verbose',
Description: 'display details',
Hint: 'yes or no!',
IconData: 'Create icon',
Suggestion: '--verbose',
},
{
Complete: 'jira issue create --project KT --epic',
Description: 'The Jira epic',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--epic',
},
]);
});
test('create flags mid-flag', async () => {
const mid = await parser.getSuggestions('/jira issue create --project KT --summ');
expect(mid).toEqual([
{
Complete: 'jira issue create --project KT --summary',
Description: 'The Jira issue summary',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--summary',
},
]);
const full = await parser.getSuggestions('/jira issue create --project KT --summary');
expect(full).toEqual([
{
Complete: 'jira issue create --project KT --summary',
Description: 'The Jira issue summary',
Hint: 'The thing is working great!',
IconData: 'Create icon',
Suggestion: '--summary',
},
]);
});
test('empty text value suggestion', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary ');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary',
Description: 'The Jira issue summary',
Hint: '',
IconData: 'Create icon',
Suggestion: 'summary: ""',
},
]);
});
test('partial text value suggestion', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary Sum');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary Sum',
Description: 'The Jira issue summary',
Hint: '',
IconData: 'Create icon',
Suggestion: 'summary: "Sum"',
},
]);
});
test('quote text value suggestion close quotes', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "Sum');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary "Sum"',
Description: 'The Jira issue summary',
Hint: '',
IconData: 'Create icon',
Suggestion: 'summary: "Sum"',
},
]);
});
test('tick text value suggestion close quotes', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary `Sum');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary `Sum`',
Description: 'The Jira issue summary',
Hint: '',
IconData: 'Create icon',
Suggestion: 'summary: `Sum`',
},
]);
});
test('create flag summary value', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --summary ');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --summary',
Description: 'The Jira issue summary',
Hint: '',
IconData: 'Create icon',
Suggestion: 'summary: ""',
},
]);
});
test('create flag project dynamic select value', async () => {
const f = Client4.executeAppCall;
Client4.executeAppCall = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
const suggestions = await parser.getSuggestions('/jira issue create --project ');
Client4.executeAppCall = f;
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project special-value',
Suggestion: 'special-value',
Description: 'special-label',
Hint: '',
IconData: 'Create icon',
},
]);
});
test('create flag epic static select value', async () => {
let suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic ');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary "great feature" --epic epic1',
Suggestion: 'Dylan Epic',
Description: 'The Jira epic',
Hint: 'The thing is working great!',
IconData: 'Create icon',
},
{
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
Suggestion: 'Michael Epic',
Description: 'The Jira epic',
Hint: 'The thing is working great!',
IconData: 'Create icon',
},
]);
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic M');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary "great feature" --epic epic2',
Suggestion: 'Michael Epic',
Description: 'The Jira epic',
Hint: 'The thing is working great!',
IconData: 'Create icon',
},
]);
suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic Nope');
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary "great feature" --epic',
Suggestion: '',
Description: '',
Hint: 'No matching options.',
IconData: 'error',
},
]);
});
test('filled out form shows execute', async () => {
const suggestions = await parser.getSuggestions('/jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true ');
if (!checkForExecuteSuggestion) {
expect(suggestions).toEqual([]);
return;
}
expect(suggestions).toEqual([
{
Complete: 'jira issue create --project KT --summary "great feature" --epic epicvalue --verbose true _execute_current_command',
Suggestion: 'Execute Current Command',
Description: 'Select this option or use Ctrl+Enter to execute the current command.',
IconData: '_execute_current_command',
Hint: '',
},
]);
});
});
describe('composeCallFromCommand', () => {
const base = {
context: {
app_id: 'jira',
channel_id: 'current_channel_id',
location: '/command',
root_id: 'root_id',
team_id: 'team_id',
},
path: '/create-issue',
};
test('empty form', async () => {
const cmd = '/jira issue create';
const values = {};
const {call} = await parser.composeCallFromCommand(cmd);
expect(call).toEqual({
...base,
raw_command: cmd,
expand: {},
query: undefined,
selected_field: undefined,
values,
});
});
test('full form', async () => {
const cmd = '/jira issue create --summary "Here it is" --epic epic1 --verbose true --project';
const values = {
summary: 'Here it is',
epic: {
label: 'Dylan Epic',
value: 'epic1',
},
verbose: 'true',
project: '',
};
const {call} = await parser.composeCallFromCommand(cmd);
expect(call).toEqual({
...base,
expand: {},
selected_field: undefined,
query: undefined,
raw_command: cmd,
values,
});
});
test('dynamic lookup test', async () => {
const f = Client4.executeAppCall;
const mockedExecute = jest.fn().mockResolvedValue(Promise.resolve({type: AppCallResponseTypes.OK, data: {items: [{label: 'special-label', value: 'special-value'}]}}));
Client4.executeAppCall = mockedExecute;
const suggestions = await parser.getSuggestions('/jira issue create --summary "The summary" --epic epic1 --project special');
Client4.executeAppCall = f;
expect(suggestions).toEqual([
{
Complete: 'jira issue create --summary "The summary" --epic epic1 --project special-value',
Suggestion: 'special-value',
Description: 'special-label',
Hint: '',
IconData: 'Create icon',
},
]);
expect(mockedExecute).toHaveBeenCalledWith({
context: {
app_id: 'jira',
channel_id: 'current_channel_id',
location: '/command',
root_id: 'root_id',
team_id: 'team_id',
},
expand: {},
path: '/create-issue',
query: 'special',
raw_command: '/jira issue create --summary "The summary" --epic epic1 --project special',
selected_field: 'project',
values: {
summary: 'The summary',
epic: {
label: 'Dylan Epic',
value: 'epic1',
},
},
}, AppCallTypes.LOOKUP);
});
});
});

View File

@@ -1,86 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type {
AppCallRequest,
AppBinding,
AppField,
AppSelectOption,
AppCallResponse,
AppCallValues,
AppContext,
AppForm,
AutocompleteElement,
AutocompleteDynamicSelect,
AutocompleteStaticSelect,
AutocompleteUserSelect,
AutocompleteChannelSelect,
} from '@mm-redux/types/apps';
import type {
AutocompleteSuggestion,
} from '@mm-redux/types/integrations';
export type {AutocompleteSuggestion};
export type {
Channel,
} from '@mm-redux/types/channels';
export type {
GlobalState,
} from '@mm-redux/types/store';
export type {
DispatchFunc,
} from '@mm-redux/types/actions';
export {
AppBindingLocations,
AppCallTypes,
AppFieldTypes,
AppCallResponseTypes,
} from '@mm-redux/constants/apps';
export {getAppsBindings} from '@mm-redux/selectors/entities/apps';
export {getPost} from '@mm-redux/selectors/entities/posts';
export {getChannel, getCurrentChannel, getChannelByName as selectChannelByName} from '@mm-redux/selectors/entities/channels';
export {getCurrentTeamId, getCurrentTeam} from '@mm-redux/selectors/entities/teams';
export {getUserByUsername as selectUserByUsername} from '@mm-redux/selectors/entities/users';
export {getUserByUsername} from '@mm-redux/actions/users';
export {getChannelByNameAndTeamName} from '@mm-redux/actions/channels';
export {doAppCall} from '@actions/apps';
export {createCallRequest} from '@utils/apps';
import Store from '@store/store';
export const getStore = () => Store.redux;
import keyMirror from '@mm-redux/utils/key_mirror';
export {keyMirror};
export const EXECUTE_CURRENT_COMMAND_ITEM_ID = '_execute_current_command';
import type {ParsedCommand} from './app_command_parser';
export const getExecuteSuggestion = (_: ParsedCommand): AutocompleteSuggestion | null => { // eslint-disable-line @typescript-eslint/no-unused-vars
return null;
};
import {Alert} from 'react-native';
import {intlShape} from 'react-intl';
export const displayError = (intl: typeof intlShape, body: string) => {
const title = intl.formatMessage({
id: 'mobile.general.error.title',
defaultMessage: 'Error',
});
Alert.alert(title, body);
};
export const errorMessage = (intl: typeof intlShape, error: string, _command: string, _position: number): string => { // eslint-disable-line @typescript-eslint/no-unused-vars
return intl.formatMessage({
id: 'apps.error.parser',
defaultMessage: 'Parsing error: {error}',
}, {
error,
});
};

View File

@@ -1,228 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
AppBinding,
AppForm,
AppFieldTypes,
} from './app_command_parser_test_dependencies';
export const reduxTestState = {
entities: {
channels: {
currentChannelId: 'current_channel_id',
myMembers: {
current_channel_id: {
channel_id: 'current_channel_id',
user_id: 'current_user_id',
roles: 'channel_role',
mention_count: 1,
msg_count: 9,
},
},
channels: {
current_channel_id: {
id: 'current_channel_id',
name: 'default-name',
display_name: 'Default',
delete_at: 0,
type: 'O',
total_msg_count: 10,
team_id: 'team_id',
},
current_user_id__existingId: {
id: 'current_user_id__existingId',
name: 'current_user_id__existingId',
display_name: 'Default',
delete_at: 0,
type: '0',
total_msg_count: 0,
team_id: 'team_id',
},
},
channelsInTeam: {
'team-id': ['current_channel_id'],
},
},
teams: {
currentTeamId: 'team-id',
teams: {
'team-id': {
id: 'team_id',
name: 'team-1',
displayName: 'Team 1',
},
},
myMembers: {
'team-id': {roles: 'team_role'},
},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_role'},
},
},
preferences: {
myPreferences: {
'display_settings--name_format': {
category: 'display_settings',
name: 'name_format',
user_id: 'current_user_id',
value: 'username',
},
},
},
roles: {
roles: {
system_role: {
permissions: [],
},
team_role: {
permissions: [],
},
channel_role: {
permissions: [],
},
},
},
general: {
license: {IsLicensed: 'false'},
serverVersion: '5.25.0',
config: {PostEditTimeLimit: -1},
},
},
};
export const viewCommand: AppBinding = {
app_id: 'jira',
label: 'view',
location: 'view',
description: 'View details of a Jira issue',
form: {
call: {
path: '/view-issue',
},
fields: [
{
name: 'project',
label: 'project',
description: 'The Jira project description',
type: AppFieldTypes.DYNAMIC_SELECT,
hint: 'The Jira project hint',
is_required: true,
},
{
name: 'issue',
position: 1,
description: 'The Jira issue key',
type: AppFieldTypes.TEXT,
hint: 'MM-11343',
is_required: true,
},
],
} as AppForm,
};
export const createCommand: AppBinding = {
app_id: 'jira',
label: 'create',
location: 'create',
description: 'Create a new Jira issue',
icon: 'Create icon',
hint: 'Create hint',
form: {
call: {
path: '/create-issue',
},
fields: [
{
name: 'project',
label: 'project',
description: 'The Jira project description',
type: AppFieldTypes.DYNAMIC_SELECT,
hint: 'The Jira project hint',
},
{
name: 'summary',
label: 'summary',
description: 'The Jira issue summary',
type: AppFieldTypes.TEXT,
hint: 'The thing is working great!',
},
{
name: 'verbose',
label: 'verbose',
description: 'display details',
type: AppFieldTypes.BOOL,
hint: 'yes or no!',
},
{
name: 'epic',
label: 'epic',
description: 'The Jira epic',
type: AppFieldTypes.STATIC_SELECT,
hint: 'The thing is working great!',
options: [
{
label: 'Dylan Epic',
value: 'epic1',
},
{
label: 'Michael Epic',
value: 'epic2',
},
],
},
],
} as AppForm,
};
export const testBindings: AppBinding[] = [
{
app_id: '',
label: '',
location: '/command',
bindings: [
{
app_id: 'jira',
label: 'jira',
description: 'Interact with your Jira instance',
icon: 'Jira icon',
hint: 'Jira hint',
bindings: [{
app_id: 'jira',
label: 'issue',
description: 'Interact with Jira issues',
icon: 'Issue icon',
hint: 'Issue hint',
bindings: [
viewCommand,
createCommand,
],
}],
},
{
app_id: 'other',
label: 'other',
description: 'Other description',
icon: 'Other icon',
hint: 'Other hint',
bindings: [{
app_id: 'other',
label: 'sub1',
description: 'Some Description',
form: {
fields: [{
name: 'fieldname',
label: 'fieldlabel',
description: 'field description',
type: AppFieldTypes.TEXT,
hint: 'field hint',
}],
},
}],
},
],
},
];

View File

@@ -1,14 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import thunk from 'redux-thunk';
export {thunk};
const configureStore = require('redux-mock-store').default;
export {configureStore};
export {Client4} from '@client/rest';
export type {AppBinding, AppForm} from '@mm-redux/types/apps';
export {AppFieldTypes} from '@mm-redux/constants/apps';
export const checkForExecuteSuggestion = false;

View File

@@ -1,18 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators, Dispatch} from 'redux';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {GlobalState} from '@mm-redux/types/store';
import {getAutocompleteCommands, getCommandAutocompleteSuggestions} from '@mm-redux/actions/integrations';
import {getAutocompleteCommandsList, getCommandAutocompleteSuggestionsList} from '@mm-redux/selectors/entities/integrations';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {appsEnabled} from '@utils/apps';
import SlashSuggestion from './slash_suggestion';
// TODO: Remove when all below commands have been implemented
@@ -28,17 +25,16 @@ const mobileCommandsSelector = createSelector(
},
);
function mapStateToProps(state: GlobalState) {
function mapStateToProps(state) {
return {
commands: mobileCommandsSelector(state),
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state),
suggestions: getCommandAutocompleteSuggestionsList(state),
appsEnabled: appsEnabled(state),
};
}
function mapDispatchToProps(dispatch: Dispatch) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getAutocompleteCommands,

View File

@@ -1,83 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {intlShape} from 'react-intl';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
Platform,
} from 'react-native';
import {analytics} from '@init/analytics';
import {Client4} from '@client/rest';
import {analytics} from '@init/analytics.ts';
import {Client4} from '@mm-redux/client';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {Command, AutocompleteSuggestion, CommandArgs} from '@mm-redux/types/integrations';
import {Theme} from '@mm-redux/types/preferences';
import {makeStyleSheetFromTheme} from '@utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
import {AppCommandParser} from './app_command_parser/app_command_parser';
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
export type Props = {
actions: {
getAutocompleteCommands: (channelID: string) => void;
getCommandAutocompleteSuggestions: (value: string, teamID: string, args: CommandArgs) => void;
export default class SlashSuggestion extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getAutocompleteCommands: PropTypes.func.isRequired,
getCommandAutocompleteSuggestions: PropTypes.func.isRequired,
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
commands: PropTypes.array,
isSearch: PropTypes.bool,
maxListHeight: PropTypes.number,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
value: PropTypes.string,
nestedScrollEnabled: PropTypes.bool,
suggestions: PropTypes.array,
rootId: PropTypes.string,
channelId: PropTypes.string,
};
currentTeamId: string;
commands: Command[];
isSearch?: boolean;
maxListHeight?: number;
theme: Theme;
onChangeText: (text: string) => void;
onResultCountChange: (count: number) => void;
value: string;
nestedScrollEnabled?: boolean;
suggestions: AutocompleteSuggestion[];
rootId?: string;
channelId: string;
appsEnabled: boolean;
};
type State = {
active: boolean;
dataSource: AutocompleteSuggestion[];
lastCommandRequest: number;
}
export default class SlashSuggestion extends PureComponent<Props, State> {
static defaultProps = {
defaultChannel: {},
value: '',
};
static contextTypes = {
intl: intlShape.isRequired,
};
appCommandParser: AppCommandParser;
state = {
active: false,
dataSource: [],
lastCommandRequest: 0,
};
constructor(props: Props, context: any) {
super(props);
this.appCommandParser = new AppCommandParser(null, context.intl, props.channelId, props.rootId);
}
setActive(active: boolean) {
setActive(active) {
this.setState({active});
}
setLastCommandRequest(lastCommandRequest: number) {
setLastCommandRequest(lastCommandRequest) {
this.setState({lastCommandRequest});
}
componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps) {
if ((this.props.value === prevProps.value && this.props.suggestions === prevProps.suggestions && this.props.commands === prevProps.commands) ||
this.props.isSearch || this.props.value.startsWith('//') || !this.props.channelId) {
return;
@@ -109,23 +88,25 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
this.setLastCommandRequest(Date.now());
}
this.showBaseCommands(nextValue, nextCommands, prevProps.channelId, prevProps.rootId);
const matches = this.filterSlashSuggestions(nextValue.substring(1), nextCommands);
this.updateSuggestions(matches);
} else if (isMinimumServerVersion(Client4.getServerVersion(), 5, 24)) {
// If this is an app command, then hand it off to the app command parser.
if (this.props.appsEnabled && this.isAppCommand(nextValue, prevProps.channelId, prevProps.rootId)) {
this.fetchAndShowAppCommandSuggestions(nextValue, prevProps.channelId, prevProps.rootId);
} else if (nextSuggestions === prevProps.suggestions) {
if (nextSuggestions === prevProps.suggestions) {
const args = {
channel_id: prevProps.channelId,
team_id: prevProps.currentTeamId,
...(prevProps.rootId && {root_id: prevProps.rootId, parent_id: prevProps.rootId}),
};
this.props.actions.getCommandAutocompleteSuggestions(nextValue, nextTeamId, args);
} else {
const matches: AutocompleteSuggestion[] = [];
nextSuggestions.forEach((suggestion: AutocompleteSuggestion) => {
if (!this.contains(matches, '/' + suggestion.Complete)) {
matches.push(suggestion);
const matches = [];
nextSuggestions.forEach((sug) => {
if (!this.contains(matches, '/' + sug.Complete)) {
matches.push({
Complete: sug.Complete,
Suggestion: sug.Suggestion,
Hint: sug.Hint,
Description: sug.Description,
});
}
});
this.updateSuggestions(matches);
@@ -135,52 +116,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
}
}
showBaseCommands = (text: string, commands: Command[], channelID: string, rootID?: string) => {
let matches: AutocompleteSuggestion[] = [];
if (this.props.appsEnabled) {
const appCommands = this.getAppBaseCommandSuggestions(text, channelID, rootID);
matches = matches.concat(appCommands);
}
matches = matches.concat(this.filterCommands(text.substring(1), commands));
matches.sort((match1, match2) => {
if (match1.Suggestion === match2.Suggestion) {
return 0;
}
return match1.Suggestion > match2.Suggestion ? 1 : -1;
});
this.updateSuggestions(matches);
}
isAppCommand = (pretext: string, channelID: string, rootID?: string) => {
this.appCommandParser.setChannelContext(channelID, rootID);
return this.appCommandParser.isAppCommand(pretext);
}
fetchAndShowAppCommandSuggestions = async (pretext: string, channelID: string, rootID?: string) => {
this.appCommandParser.setChannelContext(channelID, rootID);
const suggestions = await this.appCommandParser.getSuggestions(pretext);
this.updateSuggestions(suggestions);
}
getAppBaseCommandSuggestions = (pretext: string, channelID: string, rootID?: string): AutocompleteSuggestion[] => {
this.appCommandParser.setChannelContext(channelID, rootID);
const suggestions = this.appCommandParser.getSuggestionsBase(pretext);
return suggestions;
}
updateSuggestions = (matches: AutocompleteSuggestion[]) => {
updateSuggestions = (matches) => {
this.setState({
active: Boolean(matches.length),
active: matches.length,
dataSource: matches,
});
this.props.onResultCountChange(matches.length);
}
filterCommands = (matchTerm: string, commands: Command[]): AutocompleteSuggestion[] => {
filterSlashSuggestions = (matchTerm, commands) => {
const data = commands.filter((command) => {
if (!command.auto_complete) {
return false;
@@ -196,16 +140,15 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
Suggestion: '/' + item.trigger,
Hint: item.auto_complete_hint,
Description: item.auto_complete_desc,
IconData: item.icon_url,
};
});
}
contains = (matches: AutocompleteSuggestion[], complete: string): boolean => {
return matches.findIndex((match) => match.Complete === complete) !== -1;
contains = (matches, complete) => {
return matches.findIndex((match) => match.complete === complete) !== -1;
}
completeSuggestion = (command: string) => {
completeSuggestion = (command) => {
const {onChangeText} = this.props;
analytics.trackCommand('complete_suggestion', `/${command} `);
@@ -233,9 +176,9 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
}
};
keyExtractor = (item: Command & AutocompleteSuggestion): string => item.id || item.Suggestion;
keyExtractor = (item) => item.id || item.Suggestion;
renderItem = ({item}: {item: AutocompleteSuggestion}) => (
renderItem = ({item}) => (
<SlashSuggestionItem
description={item.Description}
hint={item.Hint}
@@ -243,7 +186,6 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
theme={this.props.theme}
suggestion={item.Suggestion}
complete={item.Complete}
icon={item.IconData}
/>
)
@@ -266,15 +208,16 @@ export default class SlashSuggestion extends PureComponent<Props, State> {
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
removeClippedSubviews={true}
renderItem={this.renderItem}
pageSize={10}
initialListSize={10}
nestedScrollEnabled={nestedScrollEnabled}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
flex: 1,

View File

@@ -1,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([]);
});
});

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -92,7 +92,6 @@ class ChannelIntro extends PureComponent {
iconSize={48}
statusBorderWidth={2}
statusSize={25}
testID='channel_intro.profile_picture'
/>
</TouchableWithFeedback>
));

View File

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

View File

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

View File

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

View File

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

View File

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