MM-35115 [Gekidou] Login flow - Email and Password (#5402)
* MM_35115: ADDED select_server screen * MM_35115: ADDED select_server screen * MM_35115: ADDED files on which select_server is dependent * MM_35115: ADDED react-native-button * MM_35115: Fixing TS issues [IN PROGRESS] * MM_35115: Started withObservables [IN PROGRESS] * MM_35115: Started withObservables [IN PROGRESS] * MM_35115: withObservables - defaulting when no connection is available [IN PROGRESS] * MM_35115: withObservables - some code clean up [IN PROGRESS] * MM_35115: withObservables - some code clean up [IN PROGRESS] * MM_35115: withObservables - some code clean up [IN PROGRESS] * MM_35115: Substituting mapDispatchToProps [IN PROGRESS] * MM_35115: Substituting mapDispatchToProps [IN PROGRESS] * MM_35115: Substituting mapDispatchToProps [IN PROGRESS] * MM_35115: Removed resetPing action [IN PROGRESS] * MM_35115: ADDED app/client * MM_35115: Preparing scheduleExpiredNotification * MM_35115: Adding some todos * Server & LoginOptions * Use default server if available and autoconnect if configured * Fix login header & manual server url * MM_35115: Login Options[IN PROGRESS] * MM_35115: Login screen - email [IN PROGRESS] * MM_35115: Login screen - email [IN PROGRESS] * MM_35115: Login screen - email - login api call [IN PROGRESS] * MM_35115: Login screen - email - login api call [IN PROGRESS] * MM_35115: Login screen - email - saving to server db [IN PROGRESS] * MM_35115: Login screen - email - saving to System, Preferences to db [IN PROGRESS] * MM_35115: Login screen - enforcing unique check on System entity [IN PROGRESS] * MM_35115: Login screen - writing TeamMembership [IN PROGRESS] * MM_35115: Login screen - writing Teams [IN PROGRESS] * MM_35115: Login screen [IN PROGRESS] * MM_35115: Login screen- Refactored DataOperator handlers [IN PROGRESS] * MM_35115: Login screen - Proper clean up [IN PROGRESS] * MM_35115: Login screen - completeLogin [IN PROGRESS] * MM_35115: Improving DataOperator * MM_35115: Improving DataOperator * MM_35115: 80% DONE - login with email and password - some todos * MM_35115: 80% DONE - login with email and password - some todos * MM_35115: 80% DONE - login with email and password - some todos * MM_35115: Removing unused app/queries folder * MM_35115: Clean up * MM_35115: Clean up * MM_35115: Clean up * MM_35115: Clean up * MM_35115: Clean up * MM_35115: Adding roles for MYTEAM * MM_35115: Code clean up * MM_35115: Code clean up * MM_35115: Code clean up * MM_35115: Adding rn-fetch-blob for Android * MM_35115: Code clean up * MM_35115: Code clean up * MM_35115: Added test setup * MM_35115: Fix database utils * MM_35115: ADDED loadRolesIfNeeded * MM_35115: Fix TS issue * MM_35115: ADDED Tests setup * MM_35115: Fix TS issues * MM_35115: Fix TS issues * MM_35115: Fix TS issues * MM_35115: Added alternative to site name * MM_35115: Added alternative to site name * MM_35115: Removed hardcoded values * MM_35115: Clean up * MM_35115 - Fixed Android platform check instead of hermes * MM_35115 - Replaced emptyErrorHandlingFunction with emptyFunction * MM_35115 : Implemented TS fixes * Update index.ts * MM-35115 - Fix react-test-renderer issue * MM_35115 - Optimizing DatabaseManager * MM_35115 : Implemented getDatabaseConnection * MM_35115 : Refactoring set/getActiveDatabase to use flag record * MM_35115 : Refactored active database to use flag in Global entity * MM_35115 : Updated manual database manager test * MM_35115 : Fix operator/utils/test * MM_35115 : Fix for base_handler * MM_35115 : Fix test issues with Handlers * MM_35115 : Fix test issues with prepareRecords * MM_35115 : Fix wrapper test issue * MM_35115 : Updated getMostRecentServerConnection to return the serverUrl as well as the connection * MM_35115 : Refactored the way we call DataOperator * MM_35115 : Updated database manager mock * Add getMostRecentServerUrl function (#5440) * fix: add getMostRecentServerUrl func * fix: add ts and tsx to editorconfig * fix: rename functions * fix: return type * Fix unit test setup * fix login screen unit tests * MM-36205 [GEKIDOU] Login Flow SSO (#5454) * MM_35115: Starting LoginOptions SSO * MM_36205: SSO [IN PROGRESS] * MM_36205 : SSO [ IN PROGRESS ] * Update sso_with_redirect_url.tsx * MM_36205 : SSO Tests [ IN PROGRESS ] * MM_36205 : Passing serverUrl to SSO screen * Update sso.test.tsx * Fix ViewTypes imports and keyMirror method * MM_36205 : Code clean up * Fix : Clean up imports * Update: Aligning with PR 5452 * Fix: AndroidManifest file to include redirection ofr scheme mmauthbeta * refactor: SSO Login method via Gitlab now navigates to Channel screen * refactor: SSO Login without redirectURL is also working * feat: SSO - main test completed * feat: ADDED test for sso_with_redirect_url * fix : eslint correction * fix: Updated Loading component name * fix : code clean up from reviews * fix: reviews check * fix: Added mmauthbeta into info.plist * Revert "fix: Added mmauthbeta into info.plist" This reverts commit d87cc23f5b451a49d9ffa9de4745224b7ecfc977. * Update Info.plist * Update AppDelegate.m * feat: ADDED Forgot Password - Test [ IN PROGRESS ] * feat: Forgot Password - Completed & Tested * fix: Including MFA screen [ IN PROGRESS ] * MFA - Properly tested * Properly testing forgot_password screen * Fix login.test.tsx * Fix SSO method calls chain * Update index.tsx * Sort imports for sceen/navigation * fix: Reviews * Update signing + act in test * Removed todo comment on MFA * feedback review * fix login tests Co-authored-by: Avinash Lingaloo <> Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * App initialization refactor (#5430) * fix: initial init refactor * fix: await isServerPresent * fix: more refactor * fix: move out launch functions * fix: remove comment * fix: update credential functions * fix: refactor launch functions * fix: deep link parsing * fix: lint change * fix: update deeplink and notification handlers * fix: indentation * fix: add relaunchApp * fix eslint * refactor launchProps and autoconnect server for deeplink * fix: use undefined * fix: define OptionalLaunchProps * fix: Android - handle server URL in push notification * fix: rename func * fix: use boolean launchError instead * fix: use DatabaseModule * fix: use DatabaseHelper instead * fix: remove unnecessary null check * fix: iOS - support for serverUrl * fix: iOS - extract serverUrl in reply action * fix: iOS - expose objc specific func * fix: remove unnecessary deviceToken param * fix: return if device is untrusted Co-authored-by: Elias Nahum <nahumhbl@gmail.com> * fix: bye bye modulePaths * fix: ios build * chore: remove unused aliases from babel.config * chore: fix dependency format in package-lock.json * chore: remove transparent window background color for android AppTheme * chore: remove mattermost.js and use index.ts as app entry * fix: login flow screens theme * fix: Launch types * chore: remove OptionalLaunchProps type * fix: url utils unit tests * chore: update en.json Co-authored-by: Elias Nahum <nahumhbl@gmail.com> Co-authored-by: Avinash Lingaloo <> Co-authored-by: Elias Nahum <nahumhbl@gmail.com> Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
This commit is contained in:
parent
8742a2b1df
commit
3ee6e673c8
207 changed files with 22050 additions and 2252 deletions
|
|
@ -7,7 +7,7 @@ end_of_line = lf
|
|||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
|
||||
[*.{js,jsx,json,html}]
|
||||
[*.{js,jsx,json,html,ts,tsx}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"extends": [
|
||||
"plugin:mattermost/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -101,3 +101,4 @@ detox/artifacts
|
|||
|
||||
#editor-settings
|
||||
.vscode
|
||||
.scannerwork
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.mattermost.rnbeta">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mattermost.rnbeta">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
|
@ -44,17 +43,16 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mattermost" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mmauthbeta" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".NotificationDismissService"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver android:name=".NotificationReplyBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name="com.reactnativenavigation.controllers.NavigationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:resizeableActivity="true"/>
|
||||
<service android:name=".NotificationDismissService" android:enabled="true" android:exported="false" />
|
||||
<receiver android:name=".NotificationReplyBroadcastReceiver" android:enabled="true" android:exported="false" />
|
||||
<activity android:name="com.reactnativenavigation.controllers.NavigationActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:resizeableActivity="true"/>
|
||||
<activity
|
||||
android:name="com.mattermost.share.ShareActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package com.mattermost.helpers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
|
@ -10,27 +7,9 @@ import com.oblador.keychain.KeychainModule;
|
|||
|
||||
|
||||
public class Credentials {
|
||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
||||
|
||||
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
|
||||
public static void getCredentialsForServer(ReactApplicationContext context, String serverUrl, ResolvePromise promise) {
|
||||
final KeychainModule keychainModule = new KeychainModule(context);
|
||||
final AsyncStorage asyncStorage = new AsyncStorage(context);
|
||||
final ArrayList<String> keys = new ArrayList<String>(1);
|
||||
keys.add(CURRENT_SERVER_URL);
|
||||
KeysReadableArray asyncStorageKeys = new KeysReadableArray() {
|
||||
@Override
|
||||
public int size() {
|
||||
return keys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int index) {
|
||||
return keys.get(index);
|
||||
}
|
||||
};
|
||||
|
||||
HashMap<String, String> asyncStorageResults = asyncStorage.multiGet(asyncStorageKeys);
|
||||
String serverUrl = asyncStorageResults.get(CURRENT_SERVER_URL);
|
||||
final WritableMap options = Arguments.createMap();
|
||||
// KeyChain module fails if `authenticationPrompt` is not set
|
||||
final WritableMap authPrompt = Arguments.createMap();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package com.mattermost.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.nozbe.watermelondb.Database;
|
||||
|
||||
public class DatabaseHelper {
|
||||
private static final String DEFAULT_DATABASE_NAME = "default.db";
|
||||
private static Database defaultDatabase;
|
||||
|
||||
private static void setDefaultDatabase(Context context) {
|
||||
String databaseName = Uri.fromFile(context.getFilesDir()).toString() + "/" + DEFAULT_DATABASE_NAME;
|
||||
defaultDatabase = new Database(databaseName, context);
|
||||
}
|
||||
|
||||
public static String getOnlyServerUrl(Context context) {
|
||||
if (defaultDatabase == null) {
|
||||
setDefaultDatabase(context);
|
||||
}
|
||||
|
||||
String emptyArray[] = {};
|
||||
String query = "SELECT url FROM servers";
|
||||
Cursor cursor = defaultDatabase.rawQuery(query, emptyArray);
|
||||
|
||||
if (cursor.getCount() == 1) {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getString(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
|
|
@ -35,11 +37,10 @@ import com.wix.reactnativenotifications.core.JsIOHelper;
|
|||
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
import com.mattermost.helpers.ResolvePromise;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
|
||||
public static final String NOTIFICATION = "notification";
|
||||
public static final String NOTIFICATION_ID = "notificationId";
|
||||
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
|
||||
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
|
||||
|
|
@ -61,6 +62,7 @@ public class CustomPushNotification extends PushNotification {
|
|||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
this.context = context;
|
||||
|
||||
createNotificationChannels();
|
||||
}
|
||||
|
||||
|
|
@ -106,8 +108,10 @@ public class CustomPushNotification extends PushNotification {
|
|||
final boolean isIdLoaded = initialData.getString("id_loaded") != null ? initialData.getString("id_loaded").equals("true") : false;
|
||||
int notificationId = MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (ackId != null) {
|
||||
notificationReceiptDelivery(ackId, postId, type, isIdLoaded, new ResolvePromise() {
|
||||
String serverUrl = initialData.getString("server_url", DatabaseHelper.getOnlyServerUrl(context));
|
||||
|
||||
if (ackId != null && serverUrl != null) {
|
||||
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (isIdLoaded) {
|
||||
|
|
@ -127,6 +131,11 @@ public class CustomPushNotification extends PushNotification {
|
|||
// so we fetch the bundle again
|
||||
final Bundle data = mNotificationProps.asBundle();
|
||||
|
||||
if (serverUrl == null) {
|
||||
String message = data.getString("message");
|
||||
data.putString("message", "Unknown Server\n" + message);
|
||||
}
|
||||
|
||||
if (channelId != null) {
|
||||
notificationId = channelId.hashCode();
|
||||
|
||||
|
|
@ -465,15 +474,18 @@ public class CustomPushNotification extends PushNotification {
|
|||
|
||||
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||
String postId = bundle.getString("post_id");
|
||||
String serverUrl = bundle.getString("server_url", DatabaseHelper.getOnlyServerUrl(context));
|
||||
|
||||
if (android.text.TextUtils.isEmpty(postId) || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
if (android.text.TextUtils.isEmpty(postId) ||
|
||||
serverUrl == null ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||
replyIntent.putExtra("pushNotification", bundle);
|
||||
replyIntent.putExtra(NOTIFICATION, bundle);
|
||||
|
||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
|
||||
mContext,
|
||||
|
|
@ -540,8 +552,8 @@ public class CustomPushNotification extends PushNotification {
|
|||
return message.replaceFirst(": ", "").trim();
|
||||
}
|
||||
|
||||
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
ReceiptDelivery.send(context, ackId, postId, type, isIdLoaded, promise);
|
||||
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||
ReceiptDelivery.send(context, ackId, serverUrl, postId, type, isIdLoaded, promise);
|
||||
}
|
||||
|
||||
private void createNotificationChannels() {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import com.facebook.soloader.SoLoader;
|
|||
|
||||
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
||||
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
||||
import org.unimodules.core.interfaces.SingletonModule;
|
||||
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,9 +48,11 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
|||
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
||||
final Bundle notification = intent.getBundleExtra(CustomPushNotification.NOTIFICATION);
|
||||
final String serverUrl = notification.getString("serverUrl");
|
||||
|
||||
|
||||
Credentials.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
||||
Credentials.getCredentialsForServer(reactApplicationContext, serverUrl, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,12 @@ import com.facebook.react.bridge.WritableMap;
|
|||
import com.mattermost.helpers.*;
|
||||
|
||||
public class ReceiptDelivery {
|
||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
||||
|
||||
private static final int[] FIBONACCI_BACKOFFS = new int[] { 0, 1, 2, 3, 5, 8 };
|
||||
|
||||
public static void send(Context context, final String ackId, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
|
||||
public static void send(Context context, final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded, ResolvePromise promise) {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
|
||||
Credentials.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
||||
Credentials.getCredentialsForServer(reactApplicationContext, serverUrl, new ResolvePromise() {
|
||||
@Override
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value instanceof Boolean && !(Boolean)value) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
</style>
|
||||
|
||||
<style name="LightTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
|
|
|
|||
43
app/client/rest/apps.ts
Normal file
43
app/client/rest/apps.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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;
|
||||
375
app/client/rest/base.ts
Normal file
375
app/client/rest/base.ts
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
// 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 {ClientOptions} from '@typings/api/client4';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import {Analytics, create} from '@init/analytics';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
import ClientError from './error';
|
||||
|
||||
export default class ClientBase {
|
||||
analytics: Analytics|undefined;
|
||||
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: ClientOptions) {
|
||||
const newOptions: ClientOptions = {...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(/\/+$/, '');
|
||||
this.analytics = create(this.url);
|
||||
}
|
||||
|
||||
// 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: Response) => {
|
||||
const serverUrl = this.getUrl();
|
||||
const parsed = urlParse(url);
|
||||
|
||||
if (response.redirected) {
|
||||
const redirectUrl = urlParse(response.url);
|
||||
|
||||
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: ClientOptions) => {
|
||||
const {data} = await this.doFetchWithResponse(url, options);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
doFetchWithResponse = async (url: string, options: ClientOptions) => {
|
||||
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]);
|
||||
}
|
||||
37
app/client/rest/bots.ts
Normal file
37
app/client/rest/bots.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@utils/helpers';
|
||||
|
||||
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;
|
||||
309
app/client/rest/channels.ts
Normal file
309
app/client/rest/channels.ts
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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) => {
|
||||
this.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[]) => {
|
||||
this.analytics.trackAPI('api_channels_create_direct');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/direct`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
createGroupChannel = async (userIds: string[]) => {
|
||||
this.analytics.trackAPI('api_channels_create_group');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/group`,
|
||||
{method: 'post', body: JSON.stringify(userIds)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteChannel = async (channelId: string) => {
|
||||
this.analytics.trackAPI('api_channels_delete', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
unarchiveChannel = async (channelId: string) => {
|
||||
this.analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/restore`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
updateChannel = async (channel: Channel) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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>) => {
|
||||
this.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}) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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 = '') => {
|
||||
this.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) => {
|
||||
this.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;
|
||||
14
app/client/rest/constants.ts
Normal file
14
app/client/rest/constants.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// 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;
|
||||
99
app/client/rest/emojis.ts
Normal file
99
app/client/rest/emojis.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
createCustomEmoji = async (emoji: CustomEmoji, imageData: any) => {
|
||||
this.analytics.trackAPI('api_emoji_custom_add');
|
||||
|
||||
// FIXME: Multipart upload with client
|
||||
// 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) => {
|
||||
this.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;
|
||||
26
app/client/rest/error.ts
Normal file
26
app/client/rest/error.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {cleanUrlForLogging} from '@utils/url';
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
47
app/client/rest/files.ts
Normal file
47
app/client/rest/files.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// 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;
|
||||
95
app/client/rest/general.ts
Normal file
95
app/client/rest/general.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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<ClientConfig>;
|
||||
getClientLicenseOld: () => Promise<any>;
|
||||
getTimezones: () => Promise<string[]>;
|
||||
getDataRetentionPolicy: () => Promise<any>;
|
||||
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
|
||||
getRedirectLocation: (urlParam: string) => Promise<Record<string, 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;
|
||||
52
app/client/rest/groups.ts
Normal file
52
app/client/rest/groups.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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;
|
||||
83
app/client/rest/index.test.js
Normal file
83
app/client/rest/index.test.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
|
||||
import {HEADER_X_VERSION_ID} from '@client/rest/constants';
|
||||
import ClientError from '@client/rest/error';
|
||||
import TestHelper from '@test/test_helper';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
describe('Client4', () => {
|
||||
beforeAll(() => {
|
||||
if (!nock.isActive()) {
|
||||
nock.activate();
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.restore();
|
||||
});
|
||||
|
||||
it('should remove trailing slash while trying to setUrl', () => {
|
||||
const client = TestHelper.createClient();
|
||||
|
||||
client.setUrl('http://localhost:8065/');
|
||||
expect(client.url).toBe('http://localhost:8065');
|
||||
|
||||
client.setUrl('http://localhost:8065/company/mattermost/');
|
||||
expect(client.url).toBe('http://localhost:8065/company/mattermost');
|
||||
});
|
||||
|
||||
describe('doFetchWithResponse', () => {
|
||||
it('serverVersion should be set from response header', async () => {
|
||||
const client = TestHelper.createClient();
|
||||
|
||||
assert.equal(client.serverVersion, '');
|
||||
|
||||
nock(client.getBaseRoute()).
|
||||
get('/users/me').
|
||||
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.0.0.5.0.0.abc123'});
|
||||
|
||||
await client.getMe();
|
||||
|
||||
assert.equal(client.serverVersion, '5.0.0.5.0.0.abc123');
|
||||
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true);
|
||||
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), false);
|
||||
|
||||
nock(client.getBaseRoute()).
|
||||
get('/users/me').
|
||||
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.3.0.5.3.0.abc123'});
|
||||
|
||||
await client.getMe();
|
||||
|
||||
assert.equal(client.serverVersion, '5.3.0.5.3.0.abc123');
|
||||
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true);
|
||||
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClientError', () => {
|
||||
it('standard fields should be enumerable', () => {
|
||||
const error = new ClientError('https://example.com', {
|
||||
message: 'This is a message',
|
||||
intl: {
|
||||
id: 'test.error',
|
||||
defaultMessage: 'This is a message with a translation',
|
||||
},
|
||||
server_error_id: 'test.app_error',
|
||||
status_code: 418,
|
||||
url: 'https://example.com/api/v4/error',
|
||||
});
|
||||
|
||||
const copy = {...error};
|
||||
|
||||
assert.strictEqual(copy.message, error.message);
|
||||
assert.strictEqual(copy.intl, error.intl);
|
||||
assert.strictEqual(copy.server_error_id, error.server_error_id);
|
||||
assert.strictEqual(copy.status_code, error.status_code);
|
||||
assert.strictEqual(copy.url, error.url);
|
||||
});
|
||||
});
|
||||
56
app/client/rest/index.ts
Normal file
56
app/client/rest/index.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// 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};
|
||||
66
app/client/rest/integrations.ts
Normal file
66
app/client/rest/integrations.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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 = {}) => {
|
||||
this.analytics.trackAPI('api_integrations_used');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}/execute`,
|
||||
{method: 'post', body: JSON.stringify({command, ...commandArgs})},
|
||||
);
|
||||
};
|
||||
|
||||
addCommand = async (command: Command) => {
|
||||
this.analytics.trackAPI('api_integrations_created');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(command)},
|
||||
);
|
||||
};
|
||||
|
||||
submitInteractiveDialog = async (data: DialogSubmission) => {
|
||||
this.analytics.trackAPI('api_interactive_messages_dialog_submitted');
|
||||
return this.doFetch(
|
||||
`${this.getBaseRoute()}/actions/dialogs/submit`,
|
||||
{method: 'post', body: JSON.stringify(data)},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default ClientIntegrations;
|
||||
234
app/client/rest/posts.ts
Normal file
234
app/client/rest/posts.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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) => {
|
||||
this.analytics.trackAPI('api_posts_create', {channel_id: post.channel_id});
|
||||
|
||||
if (post.root_id != null && post.root_id !== '') {
|
||||
this.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) => {
|
||||
this.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}) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/pinned`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
markPostAsUnread = async (userId: string, postId: string) => {
|
||||
this.analytics.trackAPI('api_post_set_unread_post');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/posts/${postId}/set_unread`,
|
||||
{method: 'post'},
|
||||
);
|
||||
}
|
||||
|
||||
pinPost = async (postId: string) => {
|
||||
this.analytics.trackAPI('api_posts_pin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/pin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
unpinPost = async (postId: string) => {
|
||||
this.analytics.trackAPI('api_posts_unpin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/unpin`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
addReaction = async (userId: string, postId: string, emojiName: string) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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) {
|
||||
this.analytics.trackAPI('api_interactive_messages_menu_selected');
|
||||
} else {
|
||||
this.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;
|
||||
33
app/client/rest/preferences.ts
Normal file
33
app/client/rest/preferences.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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;
|
||||
175
app/client/rest/teams.ts
Normal file
175
app/client/rest/teams.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {buildQueryString} from '@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) => {
|
||||
this.analytics.trackAPI('api_teams_create');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}`,
|
||||
{method: 'post', body: JSON.stringify(team)},
|
||||
);
|
||||
};
|
||||
|
||||
deleteTeam = async (teamId: string) => {
|
||||
this.analytics.trackAPI('api_teams_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}`,
|
||||
{method: 'delete'},
|
||||
);
|
||||
};
|
||||
|
||||
updateTeam = async (team: Team) => {
|
||||
this.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}) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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;
|
||||
25
app/client/rest/tos.ts
Normal file
25
app/client/rest/tos.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// 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;
|
||||
396
app/client/rest/users.ts
Normal file
396
app/client/rest/users.ts
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import {buildQueryString, isMinimumServerVersion} from '@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) => {
|
||||
this.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}) => {
|
||||
this.analytics.trackAPI('api_users_patch');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userPatch.id)}/patch`,
|
||||
{method: 'put', body: JSON.stringify(userPatch)},
|
||||
);
|
||||
}
|
||||
|
||||
updateUser = async (user: UserProfile) => {
|
||||
this.analytics.trackAPI('api_users_update');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(user.id)}`,
|
||||
{method: 'put', body: JSON.stringify(user)},
|
||||
);
|
||||
}
|
||||
|
||||
demoteUserToGuest = async (userId: string) => {
|
||||
this.analytics.trackAPI('api_users_demote_user_to_guest');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/demote`,
|
||||
{method: 'post'},
|
||||
);
|
||||
}
|
||||
|
||||
getKnownUsers = async () => {
|
||||
this.analytics.trackAPI('api_get_known_users');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/known`,
|
||||
{method: 'get'},
|
||||
);
|
||||
}
|
||||
|
||||
sendPasswordResetEmail = async (email: string) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.analytics.trackAPI('api_users_login');
|
||||
|
||||
if (ldapOnly) {
|
||||
this.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 = '') => {
|
||||
this.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 () => {
|
||||
this.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 = {}) => {
|
||||
this.analytics.trackAPI('api_profiles_get');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesByIds = async (userIds: string[], options = {}) => {
|
||||
this.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[]) => {
|
||||
this.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 = {}) => {
|
||||
this.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) => {
|
||||
this.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 = {}) => {
|
||||
this.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 = '') => {
|
||||
this.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[]) => {
|
||||
this.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) => {
|
||||
this.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) => {
|
||||
this.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;
|
||||
247
app/client/websocket/index.ts
Normal file
247
app/client/websocket/index.ts
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
|
||||
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
class WebSocketClient {
|
||||
conn?: WebSocket;
|
||||
connectionUrl: string;
|
||||
token: string|null;
|
||||
sequence: number;
|
||||
connectFailCount: number;
|
||||
eventCallback?: Function;
|
||||
firstConnectCallback?: Function;
|
||||
reconnectCallback?: Function;
|
||||
errorCallback?: Function;
|
||||
closeCallback?: Function;
|
||||
connectingCallback?: Function;
|
||||
stop: boolean;
|
||||
connectionTimeout: any;
|
||||
|
||||
constructor() {
|
||||
this.connectionUrl = '';
|
||||
this.token = null;
|
||||
this.sequence = 1;
|
||||
this.connectFailCount = 0;
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
initialize(token: string|null, opts = {}) {
|
||||
const defaults = {
|
||||
forceConnection: true,
|
||||
connectionUrl: this.connectionUrl,
|
||||
};
|
||||
|
||||
const {connectionUrl, forceConnection, ...additionalOptions} = Object.assign({}, defaults, opts);
|
||||
|
||||
if (forceConnection) {
|
||||
this.stop = false;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.conn) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionUrl == null) {
|
||||
console.log('websocket must have connection url'); //eslint-disable-line no-console
|
||||
reject(new Error('websocket must have connection url'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
}
|
||||
|
||||
const regex = /^(?:https?|wss?):(?:\/\/)?[^/]*/;
|
||||
const captured = (regex).exec(connectionUrl);
|
||||
|
||||
let origin;
|
||||
if (captured) {
|
||||
origin = captured[0];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
// this is done cause for android having the port 80 or 443 will fail the connection
|
||||
// the websocket will append them
|
||||
const split = origin.split(':');
|
||||
const port = split[2];
|
||||
if (port === '80' || port === '443') {
|
||||
origin = `${split[0]}:${split[1]}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we're unable to set the origin header, the websocket won't connect, but the URL is likely malformed anyway
|
||||
const errorMessage = 'websocket failed to parse origin from ' + connectionUrl;
|
||||
console.warn(errorMessage); // eslint-disable-line no-console
|
||||
reject(new Error(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
this.conn = new WebSocket(connectionUrl, [], {headers: {origin}, ...(additionalOptions || {})});
|
||||
this.connectionUrl = connectionUrl;
|
||||
this.token = token;
|
||||
|
||||
this.conn!.onopen = () => {
|
||||
if (token) {
|
||||
// we check for the platform as a workaround until we fix on the server that further authentications
|
||||
// are ignored
|
||||
this.sendMessage('authentication_challenge', {token});
|
||||
}
|
||||
|
||||
if (this.connectFailCount > 0) {
|
||||
console.log('websocket re-established connection'); //eslint-disable-line no-console
|
||||
if (this.reconnectCallback) {
|
||||
this.reconnectCallback();
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
this.firstConnectCallback();
|
||||
}
|
||||
|
||||
this.connectFailCount = 0;
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
this.conn!.onclose = () => {
|
||||
this.conn = undefined;
|
||||
this.sequence = 1;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
this.connectFailCount++;
|
||||
|
||||
if (this.closeCallback) {
|
||||
this.closeCallback(this.connectFailCount);
|
||||
}
|
||||
|
||||
let retryTime = MIN_WEBSOCKET_RETRY_TIME;
|
||||
|
||||
// If we've failed a bunch of connections then start backing off
|
||||
if (this.connectFailCount > MAX_WEBSOCKET_FAILS) {
|
||||
retryTime = MIN_WEBSOCKET_RETRY_TIME * this.connectFailCount;
|
||||
if (retryTime > MAX_WEBSOCKET_RETRY_TIME) {
|
||||
retryTime = MAX_WEBSOCKET_RETRY_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
this.initialize(token, opts);
|
||||
},
|
||||
retryTime,
|
||||
);
|
||||
};
|
||||
|
||||
this.conn!.onerror = (evt: any) => {
|
||||
if (this.connectFailCount <= 1) {
|
||||
console.log('websocket error'); //eslint-disable-line no-console
|
||||
console.log(evt); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
if (this.errorCallback) {
|
||||
this.errorCallback(evt);
|
||||
}
|
||||
};
|
||||
|
||||
this.conn!.onmessage = (evt: any) => {
|
||||
const msg = JSON.parse(evt.data);
|
||||
if (msg.seq_reply) {
|
||||
if (msg.error) {
|
||||
console.warn(msg); //eslint-disable-line no-console
|
||||
}
|
||||
} else if (this.eventCallback) {
|
||||
this.eventCallback(msg);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
setConnectingCallback(callback: Function) {
|
||||
this.connectingCallback = callback;
|
||||
}
|
||||
|
||||
setEventCallback(callback: Function) {
|
||||
this.eventCallback = callback;
|
||||
}
|
||||
|
||||
setFirstConnectCallback(callback: Function) {
|
||||
this.firstConnectCallback = callback;
|
||||
}
|
||||
|
||||
setReconnectCallback(callback: Function) {
|
||||
this.reconnectCallback = callback;
|
||||
}
|
||||
|
||||
setErrorCallback(callback: Function) {
|
||||
this.errorCallback = callback;
|
||||
}
|
||||
|
||||
setCloseCallback(callback: Function) {
|
||||
this.closeCallback = callback;
|
||||
}
|
||||
|
||||
close(stop = false) {
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.sequence = 1;
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.onclose = () => {}; //eslint-disable-line @typescript-eslint/no-empty-function
|
||||
this.conn.close();
|
||||
this.conn = undefined;
|
||||
console.log('websocket closed'); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(action: string, data: any) {
|
||||
const msg = {
|
||||
action,
|
||||
seq: this.sequence++,
|
||||
data,
|
||||
};
|
||||
|
||||
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
|
||||
this.conn.send(JSON.stringify(msg));
|
||||
} else if (!this.conn || this.conn.readyState === WebSocket.CLOSED) {
|
||||
this.conn = undefined;
|
||||
this.initialize(this.token);
|
||||
}
|
||||
}
|
||||
|
||||
userTyping(channelId: string, parentId: string) {
|
||||
this.sendMessage('user_typing', {
|
||||
channel_id: channelId,
|
||||
parent_id: parentId,
|
||||
});
|
||||
}
|
||||
|
||||
getStatuses() {
|
||||
this.sendMessage('get_statuses', null);
|
||||
}
|
||||
|
||||
getStatusesByIds(userIds: string[]) {
|
||||
this.sendMessage('get_statuses_by_ids', {
|
||||
user_ids: userIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new WebSocketClient();
|
||||
38
app/components/app_version/index.tsx
Normal file
38
app/components/app_version/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
info: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
version: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
const AppVersion = () => {
|
||||
return (
|
||||
<View pointerEvents='none'>
|
||||
<View style={style.info}>
|
||||
<FormattedText
|
||||
id='mobile.about.appVersion'
|
||||
defaultMessage='App Version: {version} (Build {number})'
|
||||
style={style.version}
|
||||
values={{
|
||||
version: DeviceInfo.getVersion(),
|
||||
number: DeviceInfo.getBuildNumber(),
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppVersion;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ErrorText should match snapshot 1`] = `
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#fd5960",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"marginHorizontal": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="error.text"
|
||||
>
|
||||
Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols
|
||||
</Text>
|
||||
`;
|
||||
28
app/components/error_text/error_text.test.tsx
Normal file
28
app/components/error_text/error_text.test.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {render} from '@testing-library/react-native';
|
||||
import {Preferences} from '@constants';
|
||||
|
||||
import ErrorText from './index';
|
||||
|
||||
describe('ErrorText', () => {
|
||||
const baseProps = {
|
||||
testID: 'error.text',
|
||||
textStyle: {
|
||||
fontSize: 14,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
theme: Preferences.THEMES.default,
|
||||
error: 'Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols',
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = render(
|
||||
<ErrorText {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
59
app/components/error_text/index.tsx
Normal file
59
app/components/error_text/index.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {ClientError} from '@utils/client_error';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export type ClientErrorWithIntl = ClientError & {intl: {values?: Record<string, any>}}
|
||||
|
||||
type ErrorProps = {
|
||||
error: ClientErrorWithIntl | string;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const ErrorText = ({error, testID, textStyle, theme}: ErrorProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const message = typeof (error) === 'string' ? error : error.message;
|
||||
|
||||
if (typeof (error) !== 'string' && error.intl) {
|
||||
const {intl} = error;
|
||||
return (
|
||||
<FormattedText
|
||||
testID={testID}
|
||||
id={intl.id}
|
||||
defaultMessage={intl.defaultMessage}
|
||||
values={intl.values}
|
||||
style={[style.errorLabel, textStyle]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
testID={testID}
|
||||
style={[style.errorLabel, textStyle]}
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
errorLabel: {
|
||||
color: (theme?.errorTextColor || '#DA4A4A'),
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default ErrorText;
|
||||
88
app/components/formatted_text/index.tsx
Normal file
88
app/components/formatted_text/index.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createElement, isValidElement} from 'react';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
type FormattedTextProps = {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
values?: Record<string, any>;
|
||||
testID?: string;
|
||||
style?: StyleProp<ViewStyle> | StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
const FormattedText = (props: FormattedTextProps) => {
|
||||
const intl = useIntl();
|
||||
const {formatMessage} = intl;
|
||||
const {id, defaultMessage, values, ...otherProps} = props;
|
||||
const tokenizedValues: Record<string, any> = {};
|
||||
const elements: Record<string, any> = {};
|
||||
let tokenDelimiter = '';
|
||||
|
||||
if (values && Object.keys(values).length > 0) {
|
||||
// Creates a token with a random UID that should not be guessable or
|
||||
// conflict with other parts of the `message` string.
|
||||
const uid = Math.floor(Math.random() * 0x10000000000).toString(16);
|
||||
|
||||
const generateToken = (() => {
|
||||
let counter = 0;
|
||||
return () => {
|
||||
const elementId = `ELEMENT-${uid}-${(counter += 1)}`;
|
||||
return elementId;
|
||||
};
|
||||
})();
|
||||
|
||||
// Splitting with a delimiter to support IE8. When using a regex
|
||||
// with a capture group IE8 does not include the capture group in
|
||||
// the resulting array.
|
||||
tokenDelimiter = `@__${uid}__@`;
|
||||
|
||||
// Iterates over the `props` to keep track of any React Element
|
||||
// values so they can be represented by the `token` as a placeholder
|
||||
// when the `message` is formatted. This allows the formatted
|
||||
// message to then be broken-up into parts with references to the
|
||||
// React Elements inserted back in.
|
||||
Object.keys(values).forEach((name) => {
|
||||
const value = values[name];
|
||||
|
||||
if (isValidElement(value)) {
|
||||
const token = generateToken();
|
||||
tokenizedValues[name] = tokenDelimiter + token + tokenDelimiter;
|
||||
elements[token] = value;
|
||||
} else {
|
||||
tokenizedValues[name] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const descriptor = {id, defaultMessage};
|
||||
const formattedMessage = formatMessage(
|
||||
descriptor,
|
||||
tokenizedValues || values,
|
||||
);
|
||||
const hasElements = elements && Object.keys(elements).length > 0;
|
||||
|
||||
let nodes;
|
||||
if (hasElements) {
|
||||
// Split the message into parts so the React Element values captured
|
||||
// above can be inserted back into the rendered message. This
|
||||
// approach allows messages to render with React Elements while
|
||||
// keeping React's virtual diffing working properly.
|
||||
nodes = formattedMessage.
|
||||
split(tokenDelimiter).
|
||||
filter((part) => Boolean(part)).
|
||||
map((part) => elements[part] || part);
|
||||
} else {
|
||||
nodes = [formattedMessage];
|
||||
}
|
||||
|
||||
return createElement(Text, otherProps, ...nodes);
|
||||
};
|
||||
|
||||
FormattedText.defaultProps = {
|
||||
defaultMessage: '',
|
||||
};
|
||||
|
||||
export default FormattedText;
|
||||
38
app/components/loading/index.tsx
Normal file
38
app/components/loading/index.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, StyleSheet, View, ViewStyle} from 'react-native';
|
||||
|
||||
type LoadingProps = {
|
||||
color?: string,
|
||||
size?: 'small' | 'large',
|
||||
style?: ViewStyle,
|
||||
}
|
||||
|
||||
const Loading = ({size = 'large', color = 'grey', style}: LoadingProps) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ActivityIndicator
|
||||
style={[styles.loading, style]}
|
||||
animating={true}
|
||||
size={size}
|
||||
color={color}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
loading: {
|
||||
marginLeft: 3,
|
||||
},
|
||||
});
|
||||
|
||||
export default Loading;
|
||||
17
app/components/loading/loading.stories.tsx
Normal file
17
app/components/loading/loading.stories.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
|
||||
import {storiesOf} from '@storybook/react-native';
|
||||
import {withKnobs, select} from '@storybook/addon-knobs';
|
||||
|
||||
import Loading from './index';
|
||||
|
||||
storiesOf('Loading Icon Story', module).
|
||||
addDecorator(withKnobs).
|
||||
add('loading icon', () => (
|
||||
<Loading
|
||||
size={select('size', {Large: 'large', Small: 'small'}, 'large')}
|
||||
color={select('color', {Red: 'red', Blue: 'blue', Yellow: 'yellow', Black: 'black'}, 'red')}
|
||||
/>
|
||||
));
|
||||
18
app/constants/files.ts
Normal file
18
app/constants/files.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const Files: Record<string, string[]> = {
|
||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
|
||||
CODE_TYPES: ['as', 'applescript', 'osascript', 'scpt', 'bash', 'sh', 'zsh', 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', 'coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced', 'cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp', 'cs', 'csharp', 'css', 'd', 'di', 'dart', 'delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm', 'diff', 'django', 'jinja', 'dockerfile', 'docker', 'erl', 'f90', 'f95', 'fsharp', 'fs', 'gcode', 'nc', 'go', 'groovy', 'handlebars', 'hbs', 'html.hbs', 'html.handlebars', 'hs', 'hx', 'java', 'jsp', 'js', 'jsx', 'json', 'jl', 'kt', 'ktm', 'kts', 'less', 'lisp', 'lua', 'mk', 'mak', 'md', 'mkdown', 'mkd', 'matlab', 'm', 'mm', 'objc', 'obj-c', 'ml', 'perl', 'pl', 'php', 'php3', 'php4', 'php5', 'php6', 'ps', 'ps1', 'pp', 'py', 'gyp', 'r', 'ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb', 'rs', 'scala', 'scm', 'sld', 'scss', 'st', 'sql', 'swift', 'tex', 'vbnet', 'vb', 'bas', 'vbs', 'v', 'veo', 'xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist', 'yaml'],
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg', 'tiff', 'tif'],
|
||||
PATCH_TYPES: ['patch'],
|
||||
PDF_TYPES: ['pdf'],
|
||||
PRESENTATION_TYPES: ['ppt', 'pptx'],
|
||||
SPREADSHEET_TYPES: ['xlsx', 'csv'],
|
||||
TEXT_TYPES: ['txt', 'rtf'],
|
||||
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
|
||||
WORD_TYPES: ['doc', 'docx'],
|
||||
ZIP_TYPES: ['zip'],
|
||||
};
|
||||
|
||||
export default Files;
|
||||
72
app/constants/general.ts
Normal file
72
app/constants/general.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export default {
|
||||
CONFIG_CHANGED: 'config_changed',
|
||||
SERVER_VERSION_CHANGED: 'server_version_changed',
|
||||
PAGE_SIZE_DEFAULT: 60,
|
||||
PAGE_SIZE_MAXIMUM: 200,
|
||||
LOGS_PAGE_SIZE_DEFAULT: 10000,
|
||||
PROFILE_CHUNK_SIZE: 100,
|
||||
CHANNELS_CHUNK_SIZE: 50,
|
||||
TEAMS_CHUNK_SIZE: 50,
|
||||
SEARCH_TIMEOUT_MILLISECONDS: 100,
|
||||
STATUS_INTERVAL: 60000,
|
||||
AUTOCOMPLETE_LIMIT_DEFAULT: 25,
|
||||
AUTOCOMPLETE_SPLIT_CHARACTERS: ['.', '-', '_'],
|
||||
MENTION: 'mention',
|
||||
OUT_OF_OFFICE: 'ooo',
|
||||
OFFLINE: 'offline',
|
||||
AWAY: 'away',
|
||||
ONLINE: 'online',
|
||||
DND: 'dnd',
|
||||
PERMISSIONS_ALL: 'all',
|
||||
PERMISSIONS_CHANNEL_ADMIN: 'channel_admin',
|
||||
PERMISSIONS_TEAM_ADMIN: 'team_admin',
|
||||
PERMISSIONS_SYSTEM_ADMIN: 'system_admin',
|
||||
TEAM_GUEST_ROLE: 'team_guest',
|
||||
TEAM_USER_ROLE: 'team_user',
|
||||
TEAM_ADMIN_ROLE: 'team_admin',
|
||||
CHANNEL_GUEST_ROLE: 'channel_guest',
|
||||
CHANNEL_USER_ROLE: 'channel_user',
|
||||
CHANNEL_ADMIN_ROLE: 'channel_admin',
|
||||
SYSTEM_GUEST_ROLE: 'system_guest',
|
||||
SYSTEM_USER_ROLE: 'system_user',
|
||||
SYSTEM_ADMIN_ROLE: 'system_admin',
|
||||
SYSTEM_USER_ACCESS_TOKEN_ROLE: 'system_user_access_token',
|
||||
SYSTEM_POST_ALL_ROLE: 'system_post_all',
|
||||
SYSTEM_POST_ALL_PUBLIC_ROLE: 'system_post_all_public',
|
||||
ALLOW_EDIT_POST_ALWAYS: 'always',
|
||||
ALLOW_EDIT_POST_NEVER: 'never',
|
||||
ALLOW_EDIT_POST_TIME_LIMIT: 'time_limit',
|
||||
DEFAULT_POST_EDIT_TIME_LIMIT: 300,
|
||||
RESTRICT_DIRECT_MESSAGE_ANY: 'any',
|
||||
RESTRICT_DIRECT_MESSAGE_TEAM: 'team',
|
||||
SWITCH_TO_DEFAULT_CHANNEL: 'switch_to_default_channel',
|
||||
REMOVED_FROM_CHANNEL: 'removed_from_channel',
|
||||
DEFAULT_CHANNEL: 'town-square',
|
||||
DM_CHANNEL: 'D',
|
||||
OPEN_CHANNEL: 'O',
|
||||
PRIVATE_CHANNEL: 'P',
|
||||
GM_CHANNEL: 'G',
|
||||
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
|
||||
PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn',
|
||||
STORE_REHYDRATION_COMPLETE: 'store_hydration_complete',
|
||||
OFFLINE_STORE_RESET: 'offline_store_reset',
|
||||
OFFLINE_STORE_PURGE: 'offline_store_purge',
|
||||
TEAMMATE_NAME_DISPLAY: {
|
||||
SHOW_USERNAME: 'username',
|
||||
SHOW_NICKNAME_FULLNAME: 'nickname_full_name',
|
||||
SHOW_FULLNAME: 'full_name',
|
||||
},
|
||||
SPECIAL_MENTIONS: ['all', 'channel', 'here'],
|
||||
MAX_USERS_IN_GM: 8,
|
||||
MIN_USERS_IN_GM: 3,
|
||||
MAX_GROUP_CHANNELS_FOR_PROFILES: 50,
|
||||
DEFAULT_LOCALE: 'en',
|
||||
DEFAULT_AUTOLINKED_URL_SCHEMES: ['http', 'https', 'ftp', 'mailto', 'tel', 'mattermost'],
|
||||
DISABLED: 'disabled',
|
||||
DEFAULT_ON: 'default_on',
|
||||
DEFAULT_OFF: 'default_off',
|
||||
REHYDRATED: 'app/REHYDRATED',
|
||||
};
|
||||
|
|
@ -2,9 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import Attachment from './attachment';
|
||||
import SSO from './sso';
|
||||
import Database from './database';
|
||||
import DeepLink from './deep_linking';
|
||||
import Device from './device';
|
||||
import Files from './files';
|
||||
import General from './general';
|
||||
import List from './list';
|
||||
import Navigation from './navigation';
|
||||
import Preferences from './preferences';
|
||||
|
|
@ -14,9 +16,11 @@ import WebsocketEvents from './websocket';
|
|||
|
||||
export {
|
||||
Attachment,
|
||||
SSO,
|
||||
Database,
|
||||
DeepLink,
|
||||
Device,
|
||||
Files,
|
||||
General,
|
||||
List,
|
||||
Navigation,
|
||||
Preferences,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,23 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
export const CHANNEL = 'Channel';
|
||||
export const FORGOT_PASSWORD = 'ForgotPassword';
|
||||
export const LOGIN = 'Login';
|
||||
export const LOGIN_OPTIONS = 'LoginOptions';
|
||||
export const MAIN_SIDEBAR = 'MainSidebar';
|
||||
export const MFA = 'MFA';
|
||||
export const SERVER = 'Server';
|
||||
export const SETTINGS_SIDEBAR = 'SettingsSidebar';
|
||||
export const SSO = 'SSO';
|
||||
export const THREAD = 'Thread';
|
||||
|
||||
export default {
|
||||
CHANNEL,
|
||||
FORGOT_PASSWORD,
|
||||
LOGIN,
|
||||
LOGIN_OPTIONS,
|
||||
MAIN_SIDEBAR,
|
||||
MFA,
|
||||
SERVER,
|
||||
SETTINGS_SIDEBAR,
|
||||
THREAD,
|
||||
|
|
|
|||
12
app/constants/sso.ts
Normal file
12
app/constants/sso.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from '@utils/key_mirror';
|
||||
|
||||
export default keyMirror({
|
||||
GITLAB: null,
|
||||
GOOGLE: null,
|
||||
OFFICE365: null,
|
||||
OPENID: null,
|
||||
SAML: null,
|
||||
});
|
||||
|
|
@ -76,10 +76,6 @@ const ViewTypes = keyMirror({
|
|||
|
||||
REMOVE_LAST_CHANNEL_FOR_TEAM: null,
|
||||
|
||||
GITLAB: null,
|
||||
OFFICE365: null,
|
||||
SAML: null,
|
||||
|
||||
SET_INITIAL_POST_VISIBILITY: null,
|
||||
RECEIVED_FOCUSED_POST: null,
|
||||
LOADING_POSTS: null,
|
||||
|
|
|
|||
|
|
@ -41,18 +41,23 @@ import {
|
|||
} from '@database/models/server';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||
import type {
|
||||
ActiveServerDatabaseArgs,
|
||||
import {
|
||||
DatabaseConnectionArgs,
|
||||
DatabaseInstance,
|
||||
DatabaseInstances,
|
||||
DefaultNewServerArgs,
|
||||
GetDatabaseConnectionArgs,
|
||||
Models,
|
||||
RetrievedDatabase,
|
||||
} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IGlobal from '@typings/database/global';
|
||||
import IServers from '@typings/database/servers';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
const {SERVERS} = MM_TABLES.DEFAULT;
|
||||
const {SERVERS, GLOBAL} = MM_TABLES.DEFAULT;
|
||||
const DEFAULT_DATABASE = 'default';
|
||||
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
|
||||
|
||||
if (__DEV__) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
|
@ -61,7 +66,6 @@ if (__DEV__) {
|
|||
}
|
||||
|
||||
class DatabaseManager {
|
||||
private activeDatabase: DatabaseInstance;
|
||||
private defaultDatabase: DatabaseInstance;
|
||||
private readonly defaultModels: Models;
|
||||
private readonly iOSAppGroupDatabase: string | null;
|
||||
|
|
@ -100,11 +104,58 @@ class DatabaseManager {
|
|||
TermsOfService,
|
||||
User,
|
||||
];
|
||||
|
||||
this.iOSAppGroupDatabase = null;
|
||||
this.androidFilesDirectory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getDatabaseConnection: Given a server url (serverUrl) and a flag (setAsActiveDatabase), this method will attempt
|
||||
* to retrieve an existing database connection previously created for that url. If not found, it will create a new connection and register it in the DEFAULT_DATABASE
|
||||
* @param {GetDatabaseConnectionArgs} getDatabaseConnection
|
||||
* @param {string} getDatabaseConnection.connectionName
|
||||
* @param {string} getDatabaseConnection.serverUrl
|
||||
* @param {boolean} getDatabaseConnection.setAsActiveDatabase
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
getDatabaseConnection = async ({
|
||||
serverUrl,
|
||||
setAsActiveDatabase,
|
||||
connectionName,
|
||||
}: GetDatabaseConnectionArgs) => {
|
||||
// We potentially already have this server registered; so we'll try to retrieve it if it is present under DEFAULT_DATABASE/GLOBAL entity
|
||||
const existingServers = (await this.retrieveDatabaseInstances([
|
||||
serverUrl,
|
||||
])) as RetrievedDatabase[];
|
||||
|
||||
// Since we only passed one serverUrl, we'll expect only one value in the array
|
||||
const serverDatabase = existingServers?.[0];
|
||||
|
||||
let connection: DatabaseInstance;
|
||||
|
||||
if (serverDatabase) {
|
||||
// This serverUrl has previously been registered on the app
|
||||
connection = serverDatabase.dbInstance;
|
||||
} else {
|
||||
// Or, it might be that the user has this server on the web-app but not mobile-app; so we'll need to create a new entry for this new serverUrl
|
||||
const databaseName = connectionName ?? urlParse(serverUrl).hostname;
|
||||
connection = await this.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: databaseName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (setAsActiveDatabase) {
|
||||
await this.setActiveServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* createDatabaseConnection: Creates database connection and registers the new connection into the default database. However,
|
||||
* if a database connection could not be created, it will return undefined.
|
||||
|
|
@ -114,28 +165,17 @@ class DatabaseManager {
|
|||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
||||
const {
|
||||
actionsEnabled = true,
|
||||
dbName = 'default',
|
||||
dbType = DatabaseType.DEFAULT,
|
||||
serverUrl = undefined,
|
||||
} = configs;
|
||||
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
|
||||
|
||||
try {
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? 'default' : dbName;
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? DEFAULT_DATABASE : dbName;
|
||||
|
||||
// const databaseFilePath = this.getDatabaseDirectory(databaseName);
|
||||
// const databaseFilePath = this.getDatabaseFilePath(databaseName);
|
||||
const migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
|
||||
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
||||
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
|
||||
|
||||
const adapter = new LokiJSAdapter({
|
||||
dbName: databaseName,
|
||||
migrations,
|
||||
schema,
|
||||
useWebWorker: false,
|
||||
useIncrementalIndexedDB: true,
|
||||
});
|
||||
const adapter = new LokiJSAdapter({dbName: databaseName, migrations, schema, useWebWorker: false, useIncrementalIndexedDB: true});
|
||||
|
||||
// Registers the new server connection into the DEFAULT database
|
||||
if (serverUrl && shouldAddToDefaultDatabase) {
|
||||
|
|
@ -147,7 +187,7 @@ class DatabaseManager {
|
|||
}
|
||||
return new Database({adapter, actionsEnabled, modelClasses});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('createDatabaseConnection ERROR:', e);
|
||||
}
|
||||
|
||||
|
|
@ -157,22 +197,40 @@ class DatabaseManager {
|
|||
/**
|
||||
* setActiveServerDatabase: Set the new active server database. The serverUrl is used to ensure that we do not duplicate entries in the default database.
|
||||
* This method should be called when switching to another server.
|
||||
* @param {string} displayName
|
||||
* @param {string} serverUrl
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
setActiveServerDatabase = async ({displayName, serverUrl}: ActiveServerDatabaseArgs) => {
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
setActiveServerDatabase = async (serverUrl: string) => {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
|
||||
this.activeDatabase = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
shouldAddToDefaultDatabase: Boolean(!isServerPresent),
|
||||
});
|
||||
if (defaultDatabase) {
|
||||
// retrieve recentlyViewedServers from Global entity
|
||||
const recentlyViewedServers = (await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch()) as IGlobal[];
|
||||
|
||||
if (recentlyViewedServers.length) {
|
||||
// We have previously written something for this flag
|
||||
const flagRecord = recentlyViewedServers[0];
|
||||
const recentList = Array.from(flagRecord.value);
|
||||
recentList.unshift(serverUrl);
|
||||
|
||||
// so as to avoid duplicate in this array
|
||||
const sanitizedList = Array.from(new Set(recentList));
|
||||
|
||||
await defaultDatabase.action(async () => {
|
||||
await flagRecord.update((record) => {
|
||||
record.value = sanitizedList;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// The flag hasn't been set; so we create the record
|
||||
await defaultDatabase.action(async () => {
|
||||
await defaultDatabase.collections.get(GLOBAL).create((record: IGlobal) => {
|
||||
record.name = RECENTLY_VIEWED_SERVERS;
|
||||
record.value = [serverUrl];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -181,21 +239,43 @@ class DatabaseManager {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
isServerPresent = async (serverUrl: string) => {
|
||||
const allServers = await this.getAllServers();
|
||||
const existingServer = allServers?.filter((server) => {
|
||||
return server.url === serverUrl;
|
||||
});
|
||||
return existingServer && existingServer.length > 0;
|
||||
const allServers = await this.getAllServers([serverUrl]);
|
||||
return allServers?.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
||||
* @returns {DatabaseInstance}
|
||||
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
|
||||
* @returns {string}
|
||||
*/
|
||||
getActiveServerDatabase = (): DatabaseInstance => {
|
||||
return this.activeDatabase;
|
||||
};
|
||||
getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
|
||||
if (defaultDatabase) {
|
||||
const serverRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
|
||||
if (serverRecords.length) {
|
||||
const recentServers = serverRecords[0].value as string[];
|
||||
return recentServers[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
||||
* @returns {Promise<DatabaseInstance | undefined>}
|
||||
*/
|
||||
getActiveServerDatabase = async (): Promise<DatabaseInstance> => {
|
||||
const serverUrl = await this.getActiveServerUrl();
|
||||
|
||||
if (serverUrl) {
|
||||
const serverDatabase = await this.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
|
||||
return serverDatabase;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* getDefaultDatabase : Returns the default database.
|
||||
|
|
@ -213,41 +293,31 @@ class DatabaseManager {
|
|||
* and return them to the caller.
|
||||
*
|
||||
* @param {string[]} serverUrls
|
||||
* @returns {Promise<DatabaseInstances[] | null>}
|
||||
* @returns {Promise<RetrievedDatabase[] | null>}
|
||||
*/
|
||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<DatabaseInstances[] | null> => {
|
||||
if (serverUrls?.length) {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers();
|
||||
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers(serverUrls);
|
||||
|
||||
// Filter only those servers that are present in the serverUrls array
|
||||
const servers = allServers!.filter((server: IServers) => {
|
||||
return serverUrls.includes(server.url);
|
||||
});
|
||||
// Creates server database instances
|
||||
if (allServers.length) {
|
||||
const databasePromises = allServers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
|
||||
// Creates server database instances
|
||||
if (servers.length) {
|
||||
const databasePromises = servers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
return {url, dbInstance};
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
const databaseInstances = await Promise.all(databasePromises);
|
||||
return databaseInstances;
|
||||
}
|
||||
return null;
|
||||
return {url, dbInstance, displayName};
|
||||
});
|
||||
const databaseInstances = await Promise.all(databasePromises);
|
||||
return databaseInstances;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -260,26 +330,40 @@ class DatabaseManager {
|
|||
*/
|
||||
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
||||
try {
|
||||
const defaultDB = await this.getDefaultDatabase();
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
let server: IServers;
|
||||
|
||||
if (defaultDB) {
|
||||
const serversRecords = (await defaultDB.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
||||
let result = true;
|
||||
if (defaultDatabase) {
|
||||
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
||||
server = serversRecords?.[0] ?? undefined;
|
||||
|
||||
const globalRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
const global = globalRecords?.[0] ?? undefined;
|
||||
|
||||
if (server) {
|
||||
// Perform a delete operation for this server record on the 'servers' table in default database
|
||||
await defaultDB.action(async () => {
|
||||
await defaultDatabase.action(async () => {
|
||||
await server.destroyPermanently();
|
||||
});
|
||||
|
||||
return true;
|
||||
result = result && true;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (global) {
|
||||
// filter out the deleted serverURL
|
||||
const urls = global.value as string[];
|
||||
const filtered = urls.filter((url) => url !== serverUrl);
|
||||
await defaultDatabase.action(async () => {
|
||||
await global.update((record) => {
|
||||
record.value = filtered;
|
||||
});
|
||||
});
|
||||
result = result && true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
// console.log('An error occurred while trying to delete database with name ', databaseName);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -288,11 +372,17 @@ class DatabaseManager {
|
|||
* getAllServers : Retrieves all the servers registered in the default database
|
||||
* @returns {Promise<undefined | Servers[]>}
|
||||
*/
|
||||
private getAllServers = async () => {
|
||||
private getAllServers = async (serverUrls: string[]) => {
|
||||
// Retrieve all server records from the default db
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const allServers = defaultDatabase && ((await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch()) as IServers[]);
|
||||
return allServers;
|
||||
if (defaultDatabase) {
|
||||
const allServers = (await defaultDatabase.collections.
|
||||
get(SERVERS).
|
||||
query(Q.where('url', Q.oneOf(serverUrls))).
|
||||
fetch()) as IServers[];
|
||||
return allServers;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -301,7 +391,7 @@ class DatabaseManager {
|
|||
*/
|
||||
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
this.defaultDatabase = await this.createDatabaseConnection({
|
||||
configs: {dbName: 'default'},
|
||||
configs: {dbName: DEFAULT_DATABASE},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
|
|
@ -322,7 +412,7 @@ class DatabaseManager {
|
|||
|
||||
if (defaultDatabase && !isServerPresent) {
|
||||
await defaultDatabase.action(async () => {
|
||||
const serversCollection = defaultDatabase.collections.get('servers');
|
||||
const serversCollection = defaultDatabase.collections.get(SERVERS);
|
||||
await serversCollection.create((server: IServers) => {
|
||||
server.dbPath = databaseFilePath;
|
||||
server.displayName = displayName;
|
||||
|
|
@ -333,9 +423,10 @@ class DatabaseManager {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// console.log({catchError: e});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('addServerToDefaultDatabase ERROR:', e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new DatabaseManager();
|
||||
export default DatabaseManager;
|
||||
|
|
|
|||
|
|
@ -43,23 +43,27 @@ import {
|
|||
User,
|
||||
} from '@database/models/server';
|
||||
import {serverSchema} from '@database/schema/server';
|
||||
import type {
|
||||
ActiveServerDatabaseArgs,
|
||||
import {
|
||||
DatabaseConnectionArgs,
|
||||
DatabaseInstance,
|
||||
DatabaseInstances,
|
||||
DefaultNewServerArgs,
|
||||
GetDatabaseConnectionArgs,
|
||||
MigrationEvents,
|
||||
Models,
|
||||
RetrievedDatabase,
|
||||
} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IServers from '@typings/database/servers';
|
||||
import IGlobal from '@typings/database/global';
|
||||
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
const {SERVERS} = MM_TABLES.DEFAULT;
|
||||
const {SERVERS, GLOBAL} = MM_TABLES.DEFAULT;
|
||||
const DEFAULT_DATABASE = 'default';
|
||||
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
|
||||
|
||||
class DatabaseManager {
|
||||
private activeDatabase: DatabaseInstance;
|
||||
private defaultDatabase: DatabaseInstance;
|
||||
private readonly defaultModels: Models;
|
||||
private readonly iOSAppGroupDatabase: string | null;
|
||||
|
|
@ -103,6 +107,50 @@ class DatabaseManager {
|
|||
this.androidFilesDirectory = Platform.OS === 'android' ? FileSystem.documentDirectory : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* getDatabaseConnection: Given a server url (serverUrl) and a flag (setAsActiveDatabase), this method will attempt
|
||||
* to retrieve an existing database connection previously created for that url.
|
||||
* If not found, it will create a new connection and register it in the DEFAULT_DATABASE. In this case, it will also
|
||||
* use the provided connectionName as display_name for this server
|
||||
* @param {GetDatabaseConnectionArgs} getDatabaseConnection
|
||||
* @param {string} getDatabaseConnection.connectionName
|
||||
* @param {string} getDatabaseConnection.serverUrl
|
||||
* @param {boolean} getDatabaseConnection.setAsActiveDatabase
|
||||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
getDatabaseConnection = async ({serverUrl, setAsActiveDatabase, connectionName}: GetDatabaseConnectionArgs) => {
|
||||
// We potentially already have this server registered; so we'll try to retrieve it if it is present under DEFAULT_DATABASE/GLOBAL entity
|
||||
const existingServers = await this.retrieveDatabaseInstances([serverUrl]) as RetrievedDatabase[];
|
||||
|
||||
// Since we only passed one serverUrl, we'll expect only one value in the array
|
||||
const serverDatabase = existingServers?.[0];
|
||||
|
||||
let connection: DatabaseInstance;
|
||||
|
||||
if (serverDatabase) {
|
||||
// This serverUrl has previously been registered on the app
|
||||
connection = serverDatabase.dbInstance;
|
||||
} else {
|
||||
// Or, it might be that the user has this server on the web-app but not mobile-app; so we'll need to create a new entry for this new serverUrl
|
||||
const databaseName = connectionName ?? urlParse(serverUrl).hostname;
|
||||
connection = await this.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: databaseName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (setAsActiveDatabase) {
|
||||
await this.setActiveServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* createDatabaseConnection: Creates database connection and registers the new connection into the default database. However,
|
||||
* if a database connection could not be created, it will return undefined.
|
||||
|
|
@ -112,16 +160,12 @@ class DatabaseManager {
|
|||
* @returns {Promise<DatabaseInstance>}
|
||||
*/
|
||||
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
||||
const {
|
||||
actionsEnabled = true,
|
||||
dbName = 'default',
|
||||
dbType = DatabaseType.DEFAULT,
|
||||
serverUrl = undefined,
|
||||
} = configs;
|
||||
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
|
||||
|
||||
try {
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? 'default' : dbName;
|
||||
const databaseFilePath = this.getDatabaseDirectory(databaseName);
|
||||
const databaseName = dbType === DatabaseType.DEFAULT ? DEFAULT_DATABASE : dbName;
|
||||
|
||||
const databaseFilePath = this.getDatabaseFilePath(databaseName);
|
||||
const migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
|
||||
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
||||
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
|
||||
|
|
@ -153,46 +197,73 @@ class DatabaseManager {
|
|||
/**
|
||||
* setActiveServerDatabase: Set the new active server database. The serverUrl is used to ensure that we do not duplicate entries in the default database.
|
||||
* This method should be called when switching to another server.
|
||||
* @param {string} displayName
|
||||
* @param {string} serverUrl
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
setActiveServerDatabase = async ({displayName, serverUrl}: ActiveServerDatabaseArgs) => {
|
||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
||||
setActiveServerDatabase = async (serverUrl: string) => {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
if (defaultDatabase) {
|
||||
// retrieve recentlyViewedServers from Global entity
|
||||
const recentlyViewedServers = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
|
||||
this.activeDatabase = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
shouldAddToDefaultDatabase: Boolean(!isServerPresent),
|
||||
});
|
||||
if (recentlyViewedServers.length) {
|
||||
// We have previously written something for this flag
|
||||
const flagRecord = recentlyViewedServers[0];
|
||||
const recentList = Array.from(flagRecord.value);
|
||||
recentList.unshift(serverUrl);
|
||||
|
||||
// so as to avoid duplicate in this array
|
||||
const sanitizedList = Array.from(new Set(recentList));
|
||||
|
||||
await defaultDatabase.action(async () => {
|
||||
await flagRecord.update((record) => {
|
||||
record.value = sanitizedList;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// The flag hasn't been set; so we create the record
|
||||
await defaultDatabase.action(async () => {
|
||||
await defaultDatabase.collections.get(GLOBAL).create((record: IGlobal) => {
|
||||
record.name = RECENTLY_VIEWED_SERVERS;
|
||||
record.value = [serverUrl];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* isServerPresent : Confirms if the current serverUrl does not already exist in the database
|
||||
* @param {String} serverUrl
|
||||
* @returns {Promise<boolean>}
|
||||
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
private isServerPresent = async (serverUrl: string) => {
|
||||
const allServers = await this.getAllServers();
|
||||
getActiveServerUrl = async (): Promise<string | undefined> => {
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
|
||||
const existingServer = allServers?.filter((server) => {
|
||||
return server.url === serverUrl;
|
||||
});
|
||||
if (defaultDatabase) {
|
||||
const serverRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
|
||||
return existingServer && existingServer?.length > 0;
|
||||
};
|
||||
if (serverRecords.length) {
|
||||
const recentServers = serverRecords[0].value as string[];
|
||||
return recentServers[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
||||
* @returns {DatabaseInstance}
|
||||
* @returns {Promise<MostRecentConnection | undefined>}
|
||||
*/
|
||||
getActiveServerDatabase = (): DatabaseInstance => {
|
||||
return this.activeDatabase;
|
||||
getActiveServerDatabase = async (): Promise<DatabaseInstance> => {
|
||||
const serverUrl = await this.getActiveServerUrl();
|
||||
|
||||
if (serverUrl) {
|
||||
const serverDatabase = await this.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
|
||||
return serverDatabase;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -211,41 +282,32 @@ class DatabaseManager {
|
|||
* and return them to the caller.
|
||||
*
|
||||
* @param {string[]} serverUrls
|
||||
* @returns {Promise<{url: string, dbInstance: DatabaseInstance}[] | null>}
|
||||
* @returns {Promise<RetrievedDatabase[] | null>}
|
||||
*/
|
||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<DatabaseInstances[] | null> => {
|
||||
if (serverUrls?.length) {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers();
|
||||
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
|
||||
// Retrieve all server records from the default db
|
||||
const allServers = await this.getAllServers(serverUrls);
|
||||
|
||||
// Filter only those servers that are present in the serverUrls array
|
||||
const servers = allServers!.filter((server: IServers) => {
|
||||
return serverUrls.includes(server.url);
|
||||
});
|
||||
// Creates server database instances
|
||||
if (allServers.length) {
|
||||
const databasePromises = await allServers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
|
||||
// Creates server database instances
|
||||
if (servers.length) {
|
||||
const databasePromises = await servers.map(async (server: IServers) => {
|
||||
const {displayName, url} = server;
|
||||
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
return {url, dbInstance};
|
||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||
const dbInstance = await this.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: displayName,
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: url,
|
||||
},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
const databaseInstances = await Promise.all(databasePromises);
|
||||
return databaseInstances;
|
||||
}
|
||||
return null;
|
||||
return {dbInstance, displayName, url};
|
||||
});
|
||||
const databaseInstances = await Promise.all(databasePromises);
|
||||
return databaseInstances;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -258,24 +320,26 @@ class DatabaseManager {
|
|||
*/
|
||||
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
||||
try {
|
||||
const defaultDB = await this.getDefaultDatabase();
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
let server: IServers;
|
||||
let result = true;
|
||||
|
||||
if (defaultDB) {
|
||||
const serversRecords = (await defaultDB.collections.
|
||||
get(SERVERS).
|
||||
query(Q.where('url', serverUrl)).
|
||||
fetch()) as IServers[];
|
||||
if (defaultDatabase) {
|
||||
// NOTE: We are deleting this 'database' entry in the SERVERS entity on the DEFAULT database; for that we retrieve its record first.
|
||||
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
||||
server = serversRecords?.[0] ?? undefined;
|
||||
|
||||
const globalRecords = await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
const global = globalRecords?.[0] ?? undefined;
|
||||
|
||||
if (server) {
|
||||
const databaseName = server.displayName;
|
||||
|
||||
// Perform a delete operation for this server record on the 'servers' table in default database
|
||||
await defaultDB.action(async () => {
|
||||
await defaultDatabase.action(async () => {
|
||||
await server.destroyPermanently();
|
||||
});
|
||||
|
||||
const databaseName = server.displayName;
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
|
||||
deleteIOSDatabase({databaseName});
|
||||
|
|
@ -290,13 +354,24 @@ class DatabaseManager {
|
|||
await FileSystem.deleteAsync(databaseFile);
|
||||
await FileSystem.deleteAsync(databaseJournal);
|
||||
|
||||
return true;
|
||||
result = result && true;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (global) {
|
||||
// filter out the deleted serverURL
|
||||
const urls = global.value as string[];
|
||||
const filtered = urls.filter((url) => url !== serverUrl);
|
||||
await defaultDatabase.action(async () => {
|
||||
await global.update((record) => {
|
||||
record.value = filtered;
|
||||
});
|
||||
});
|
||||
result = result && true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
// console.log('An error occurred while trying to delete database with name ', databaseName);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -308,7 +383,7 @@ class DatabaseManager {
|
|||
*/
|
||||
factoryReset = async (shouldRemoveDirectory: boolean): Promise<boolean> => {
|
||||
try {
|
||||
//On iOS, we'll delete the databases folder under the shared AppGroup folder
|
||||
//On iOS, we'll delete the databases folder under the shared AppGroup folder
|
||||
if (Platform.OS === 'ios') {
|
||||
deleteIOSDatabase({shouldRemoveDirectory});
|
||||
return true;
|
||||
|
|
@ -319,20 +394,34 @@ class DatabaseManager {
|
|||
await FileSystem.deleteAsync(androidFilesDir);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// console.log('An error occurred while trying to delete the databases directory);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* isServerPresent : Confirms if the current serverUrl does not already exist in the database
|
||||
* @param {String} serverUrl
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
private isServerPresent = async (serverUrl: string) => {
|
||||
const allServers = await this.getAllServers([serverUrl]);
|
||||
return allServers?.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* getAllServers : Retrieves all the servers registered in the default database
|
||||
* @returns {Promise<undefined | Servers[]>}
|
||||
* @returns {Promise<Servers[]>}
|
||||
*/
|
||||
private getAllServers = async () => {
|
||||
private getAllServers = async (serverUrls: string[]) => {
|
||||
// Retrieve all server records from the default db
|
||||
const defaultDatabase = await this.getDefaultDatabase();
|
||||
const allServers = defaultDatabase && ((await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch()) as IServers[]);
|
||||
return allServers;
|
||||
|
||||
if (defaultDatabase) {
|
||||
const allServers = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', Q.oneOf(serverUrls))).fetch() as IServers[]);
|
||||
return allServers;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -341,7 +430,7 @@ class DatabaseManager {
|
|||
*/
|
||||
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||
this.defaultDatabase = await this.createDatabaseConnection({
|
||||
configs: {dbName: 'default'},
|
||||
configs: {dbName: DEFAULT_DATABASE},
|
||||
shouldAddToDefaultDatabase: false,
|
||||
});
|
||||
|
||||
|
|
@ -362,7 +451,7 @@ class DatabaseManager {
|
|||
|
||||
if (defaultDatabase && !isServerPresent) {
|
||||
await defaultDatabase.action(async () => {
|
||||
const serversCollection = defaultDatabase.collections.get('servers');
|
||||
const serversCollection = defaultDatabase.collections.get(SERVERS);
|
||||
await serversCollection.create((server: IServers) => {
|
||||
server.dbPath = databaseFilePath;
|
||||
server.displayName = displayName;
|
||||
|
|
@ -408,7 +497,7 @@ class DatabaseManager {
|
|||
};
|
||||
|
||||
/**
|
||||
* getDatabaseDirectory: Using the database name, this method will return the database directory for each platform.
|
||||
* getDatabaseFilePath: Using the database name, this method will return the database file path for each platform.
|
||||
* On iOS, it will point towards the AppGroup shared directory while on Android, it will point towards the Files Directory.
|
||||
* Please note that in each case, the *.db files will be created/grouped under a 'databases' sub-folder.
|
||||
* iOS Simulator : appGroup => /Users/{username}/Library/Developer/CoreSimulator/Devices/DA6F1C73/data/Containers/Shared/AppGroup/ACA65327/databases"}
|
||||
|
|
@ -417,7 +506,7 @@ class DatabaseManager {
|
|||
* @param {string} dbName
|
||||
* @returns {string}
|
||||
*/
|
||||
private getDatabaseDirectory = (dbName: string): string => {
|
||||
private getDatabaseFilePath = (dbName: string): string => {
|
||||
return Platform.OS === 'ios' ? `${this.iOSAppGroupDatabase}/${dbName}.db` : `${FileSystem.documentDirectory}${dbName}.db`;
|
||||
};
|
||||
}
|
||||
|
|
@ -428,4 +517,4 @@ if (!__DEV__) {
|
|||
logger.silence();
|
||||
}
|
||||
|
||||
export default new DatabaseManager();
|
||||
export default DatabaseManager;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {DatabaseInstance} from '@typings/database/database';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
import IGlobal from '@typings/database/global';
|
||||
import IServers from '@typings/database/servers';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
const {SERVERS} = MM_TABLES.DEFAULT;
|
||||
const {GLOBAL, SERVERS} = MM_TABLES.DEFAULT;
|
||||
const RECENTLY_VIEWED_SERVERS = 'RECENTLY_VIEWED_SERVERS';
|
||||
|
||||
// NOTE : On the mock Database Manager, we cannot test for :
|
||||
// 1. Android/iOS file path
|
||||
|
|
@ -21,12 +22,43 @@ const {SERVERS} = MM_TABLES.DEFAULT;
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Database Manager tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager | null;
|
||||
|
||||
beforeEach(() => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
databaseManagerClient = null;
|
||||
});
|
||||
|
||||
const createTwoConnections = async () => {
|
||||
await databaseManagerClient!.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'connection1',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv1.mattermost.com',
|
||||
},
|
||||
});
|
||||
await databaseManagerClient!.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'connection2',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'https://appv2.mattermost.com',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('=> should return a default database', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnAddServerToDefaultDatabase = jest.spyOn(DatabaseManager as any, 'addServerToDefaultDatabase');
|
||||
const spyOnAddServerToDefaultDatabase = jest.spyOn(databaseManagerClient as any, 'addServerToDefaultDatabase');
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
|
||||
|
||||
expect(defaultDB).toBeInstanceOf(Database);
|
||||
expect(spyOnAddServerToDefaultDatabase).not.toHaveBeenCalledTimes(1);
|
||||
|
|
@ -35,9 +67,9 @@ describe('*** Database Manager tests ***', () => {
|
|||
it('=> should create a new server connection', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnAddServerToDefaultDatabase = jest.spyOn(DatabaseManager as any, 'addServerToDefaultDatabase');
|
||||
const spyOnAddServerToDefaultDatabase = jest.spyOn(databaseManagerClient as any, 'addServerToDefaultDatabase');
|
||||
|
||||
const connection1 = await DatabaseManager.createDatabaseConnection({
|
||||
const connection1 = await databaseManagerClient!.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -52,53 +84,59 @@ describe('*** Database Manager tests ***', () => {
|
|||
});
|
||||
|
||||
it('=> should switch between active server connections', async () => {
|
||||
expect.assertions(5);
|
||||
let activeServer: DatabaseInstance;
|
||||
expect.assertions(6);
|
||||
let adapter;
|
||||
|
||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
||||
const activeServerA = await databaseManagerClient!.getActiveServerDatabase();
|
||||
|
||||
// as we haven't set an active server yet, we should be getting undefined in the activeServer variable
|
||||
expect(activeServer).toBeUndefined();
|
||||
expect(activeServerA).toBeUndefined();
|
||||
|
||||
const setActiveServer = async ({displayName, serverUrl}:{displayName: string, serverUrl: string}) => {
|
||||
const setActiveServer = async (serverUrl: string) => {
|
||||
// now we set the active database
|
||||
await DatabaseManager.setActiveServerDatabase({displayName, serverUrl});
|
||||
await databaseManagerClient!.setActiveServerDatabase(serverUrl);
|
||||
};
|
||||
|
||||
await setActiveServer({displayName: 'community mattermost', serverUrl: 'https://appv1.mattermost.com'});
|
||||
await setActiveServer('https://appv1.mattermost.com');
|
||||
|
||||
// let's verify if we now have a value for activeServer
|
||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
||||
expect(activeServer).toBeDefined();
|
||||
const activeServerB = await databaseManagerClient!.getActiveServerDatabase();
|
||||
expect(activeServerB).toBeDefined();
|
||||
|
||||
adapter = activeServer!.adapter as any;
|
||||
adapter = activeServerB!.adapter as any;
|
||||
const currentDBName = adapter.underlyingAdapter._dbName;
|
||||
expect(currentDBName).toStrictEqual('community mattermost');
|
||||
expect(currentDBName).toStrictEqual('appv1.mattermost.com');
|
||||
|
||||
// spice things up; we'll set a new server and verify if the value of activeServer changes
|
||||
await setActiveServer({displayName: 'appv2', serverUrl: 'https://appv2.mattermost.com'});
|
||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
||||
expect(activeServer).toBeDefined();
|
||||
await setActiveServer('https://appv2.mattermost.com');
|
||||
const activeServerC = await databaseManagerClient!.getActiveServerDatabase();
|
||||
expect(activeServerC).toBeDefined();
|
||||
|
||||
adapter = activeServer!.adapter as any;
|
||||
adapter = activeServerC!.adapter as any;
|
||||
const newDBName = adapter.underlyingAdapter._dbName;
|
||||
expect(newDBName).toStrictEqual('appv2');
|
||||
expect(newDBName).toStrictEqual('appv2.mattermost.com');
|
||||
|
||||
const defaultDatabase = await databaseManagerClient!.getDefaultDatabase();
|
||||
const records = await defaultDatabase!.collections.get(MM_TABLES.DEFAULT.GLOBAL).query(Q.where('name', 'RECENTLY_VIEWED_SERVERS')).fetch() as IGlobal[];
|
||||
const recentlyViewedServers = records?.[0]?.value;
|
||||
expect(recentlyViewedServers?.length).toBe(2);
|
||||
});
|
||||
|
||||
it('=> should retrieve all database instances matching serverUrls parameter', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const spyOnCreateDatabaseConnection = jest.spyOn(DatabaseManager, 'createDatabaseConnection');
|
||||
await createTwoConnections();
|
||||
|
||||
const dbInstances = await DatabaseManager.retrieveDatabaseInstances([
|
||||
const spyOnCreateDatabaseConnection = jest.spyOn(databaseManagerClient!, 'createDatabaseConnection');
|
||||
|
||||
const dbInstances = await databaseManagerClient!.retrieveDatabaseInstances([
|
||||
'https://xunity2.mattermost.com',
|
||||
'https://appv2.mattermost.com',
|
||||
'https://appv1.mattermost.com',
|
||||
]);
|
||||
|
||||
expect(dbInstances).toBeTruthy();
|
||||
const numDbInstances = dbInstances && dbInstances.length ? dbInstances.length : 0;
|
||||
const numDbInstances = dbInstances?.length ?? 0;
|
||||
|
||||
// The Database Manager will call the 'createDatabaseConnection' method in consequence of the number of database connection present in dbInstances array
|
||||
expect(spyOnCreateDatabaseConnection).toHaveBeenCalledTimes(numDbInstances);
|
||||
|
|
@ -107,12 +145,23 @@ describe('*** Database Manager tests ***', () => {
|
|||
expect(numDbInstances).toEqual(2);
|
||||
});
|
||||
|
||||
it('=> should retrieve existing database instances matching serverUrl parameter', async () => {
|
||||
expect.assertions(2);
|
||||
await createTwoConnections();
|
||||
const spyOnRetrieveDatabaseInstances = jest.spyOn(databaseManagerClient!, 'retrieveDatabaseInstances');
|
||||
const connection = await databaseManagerClient!.getDatabaseConnection({serverUrl: 'https://appv1.mattermost.com', setAsActiveDatabase: false});
|
||||
expect(spyOnRetrieveDatabaseInstances).toHaveBeenCalledTimes(1);
|
||||
expect(connection).toBeDefined();
|
||||
});
|
||||
|
||||
//todo: test the current active database together with the getDatabaseConnection method
|
||||
|
||||
it('=> should have records of Servers set in the servers table of the default database', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
|
||||
expect(defaultDB).toBeDefined();
|
||||
|
||||
await createTwoConnections();
|
||||
const serversRecords = await defaultDB!.collections.get(SERVERS).query().fetch() as IServers[];
|
||||
expect(serversRecords).toBeDefined();
|
||||
|
||||
|
|
@ -122,16 +171,29 @@ describe('*** Database Manager tests ***', () => {
|
|||
|
||||
it('=> should delete appv1 server from the servers table of Default database', async () => {
|
||||
expect.assertions(3);
|
||||
await createTwoConnections();
|
||||
|
||||
const defaultDatabase = await databaseManagerClient!.getDefaultDatabase();
|
||||
|
||||
await databaseManagerClient?.setActiveServerDatabase('https://appv1.mattermost.com');
|
||||
await databaseManagerClient?.setActiveServerDatabase('https://appv2.mattermost.com');
|
||||
|
||||
const fetchGlobalRecords = async () => {
|
||||
const initialGlobalRecords = await defaultDatabase!.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch() as IGlobal[];
|
||||
return initialGlobalRecords?.[0].value as string[];
|
||||
};
|
||||
|
||||
const recentServers = await fetchGlobalRecords();
|
||||
expect(recentServers.length).toBe(2);
|
||||
|
||||
// Removing database for appv1 connection
|
||||
const isAppV1Removed = await DatabaseManager.deleteDatabase('https://appv1.mattermost.com');
|
||||
const isAppV1Removed = await databaseManagerClient!.deleteDatabase('https://appv1.mattermost.com');
|
||||
expect(isAppV1Removed).toBe(true);
|
||||
|
||||
// Verifying in the database to confirm if its record was deleted
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
const serversRecords = await defaultDB!.collections.get(SERVERS).query().fetch() as IServers[];
|
||||
expect(serversRecords).toBeDefined();
|
||||
expect(serversRecords.length).toEqual(1);
|
||||
|
||||
const updatedRecentServers = await fetchGlobalRecords();
|
||||
expect(updatedRecentServers.length).toBe(1);
|
||||
});
|
||||
|
||||
it('=> should enforce uniqueness of connections using serverUrl as key', async () => {
|
||||
|
|
@ -139,7 +201,7 @@ describe('*** Database Manager tests ***', () => {
|
|||
|
||||
// We can't have more than one connection with the same server url
|
||||
const serverUrl = 'https://appv3.mattermost.com';
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
await databaseManagerClient!.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -149,7 +211,7 @@ describe('*** Database Manager tests ***', () => {
|
|||
},
|
||||
});
|
||||
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
await databaseManagerClient!.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -159,7 +221,7 @@ describe('*** Database Manager tests ***', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
|
||||
|
||||
const allServers = defaultDB && await defaultDB.collections.get(SERVERS).query().fetch() as IServers[];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import {Platform} from 'react-native';
|
|||
import {DatabaseType} from '@typings/database/enums';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
import DBManager from './index';
|
||||
import DatabaseManager from './index';
|
||||
|
||||
export default async () => {
|
||||
const databaseClient = new DatabaseManager();
|
||||
|
||||
// Test: It should return the iOS App-Group shared directory
|
||||
const testAppGroupDirectory = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
|
|
@ -18,12 +20,12 @@ export default async () => {
|
|||
|
||||
// Test: It should return an instance of the default database
|
||||
const testGetDefaultDatabase = () => {
|
||||
DBManager.getDefaultDatabase();
|
||||
databaseClient.getDefaultDatabase();
|
||||
};
|
||||
|
||||
// Test: It should creates a new server connection
|
||||
const testNewServerConnection = async () => {
|
||||
await DBManager.createDatabaseConnection({
|
||||
await databaseClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -36,20 +38,17 @@ export default async () => {
|
|||
|
||||
// Test: It should return the current active server database
|
||||
const testGetActiveServerConnection = () => {
|
||||
// const activeServer = DBManager.getActiveServerDatabase();
|
||||
// const activeServer = DatabaseManager.getActiveServerDatabase();
|
||||
};
|
||||
|
||||
// Test: It should set the current active server database to the provided server url.
|
||||
const testSetActiveServerConnection = async () => {
|
||||
await DBManager.setActiveServerDatabase({
|
||||
displayName: 'comm4',
|
||||
serverUrl: 'https://comm4.mattermost.com',
|
||||
});
|
||||
await databaseClient.setActiveServerDatabase('https://comm4.mattermost.com');
|
||||
};
|
||||
|
||||
// Test: It should return database instance(s) if there are valid server urls in the provided list.
|
||||
const testRetrieveAllDatabaseConnections = async () => {
|
||||
await DBManager.retrieveDatabaseInstances([
|
||||
await databaseClient.retrieveDatabaseInstances([
|
||||
'https://xunity2.mattermost.com',
|
||||
'https://comm5.mattermost.com',
|
||||
'https://comm4.mattermost.com',
|
||||
|
|
@ -58,12 +57,12 @@ export default async () => {
|
|||
|
||||
// Test: It should delete the associated *.db file for this server url
|
||||
const testDeleteSQLFile = async () => {
|
||||
await DBManager.deleteDatabase('https://comm4.mattermost.com');
|
||||
await databaseClient.deleteDatabase('https://comm4.mattermost.com');
|
||||
};
|
||||
|
||||
// Test: It should wipe out the databases folder under the documents direction on Android and in the shared directory for the AppGroup on iOS
|
||||
const testFactoryReset = async () => {
|
||||
await DBManager.factoryReset(true);
|
||||
await databaseClient.factoryReset(true);
|
||||
};
|
||||
|
||||
// NOTE : Comment and test the below functions one at a time. It starts with creating a default database and ends with a factory reset.
|
||||
|
|
|
|||
|
|
@ -22,5 +22,5 @@ export default class Global extends Model {
|
|||
@field('name') name!: string;
|
||||
|
||||
/** value : The value part of the key-value combination */
|
||||
@json('value', (rawJson) => rawJson) value!: string;
|
||||
@json('value', (rawJson) => rawJson) value!: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const isRecordRoleEqualToRaw = (record: Role, raw: RawRole) => {
|
|||
};
|
||||
|
||||
export const isRecordSystemEqualToRaw = (record: System, raw: RawSystem) => {
|
||||
return raw.id === record.id;
|
||||
return raw.name === record.name;
|
||||
};
|
||||
|
||||
export const isRecordTermsOfServiceEqualToRaw = (record: TermsOfService, raw: RawTermsOfService) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DataOperator} from '@database/operator';
|
||||
import Operator from '@database/operator';
|
||||
import {
|
||||
isRecordAppEqualToRaw,
|
||||
isRecordCustomEmojiEqualToRaw,
|
||||
|
|
@ -24,22 +24,40 @@ import {
|
|||
} from '@database/operator/prepareRecords/general';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {RawGlobal, RawRole, RawServers, RawTermsOfService} from '@typings/database/database';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
import {DatabaseType, IsolatedEntities} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleApp: should write to APP entity', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(defaultDatabase).toBeTruthy();
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.APP,
|
||||
values: [
|
||||
{
|
||||
|
|
@ -81,13 +99,13 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(defaultDatabase).toBeTruthy();
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
const values: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}];
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.GLOBAL,
|
||||
values,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -106,10 +124,10 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
it('=> HandleServers: should write to SERVERS entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(defaultDatabase).toBeTruthy();
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
const values: RawServers[] = [
|
||||
{
|
||||
|
|
@ -121,7 +139,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.SERVERS,
|
||||
values,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -150,7 +168,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
|
||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
const values: RawRole[] = [
|
||||
{
|
||||
|
|
@ -160,7 +178,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.ROLE,
|
||||
values,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -185,11 +203,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
||||
values: [
|
||||
{
|
||||
|
|
@ -229,11 +247,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
|
||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
const values = [{id: 'system-id-1', name: 'system-1', value: 'system-1'}];
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -241,7 +259,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
|
||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||
findMatchingRecordBy: isRecordSystemEqualToRaw,
|
||||
fieldName: 'id',
|
||||
fieldName: 'name',
|
||||
operator: prepareSystemRecord,
|
||||
rawValues: values,
|
||||
tableName: 'System',
|
||||
|
|
@ -254,7 +272,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
|
||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
const values: RawTermsOfService[] = [
|
||||
{
|
||||
|
|
@ -266,7 +284,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
await DataOperator.handleIsolatedEntity({
|
||||
await operatorClient.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.TERMS_OF_SERVICE,
|
||||
values,
|
||||
prepareRecordsOnly: false,
|
||||
|
|
@ -285,11 +303,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
|||
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
||||
expect(defaultDB).toBeTruthy();
|
||||
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(defaultDatabase).toBeTruthy();
|
||||
|
||||
await expect(
|
||||
DataOperator.handleIsolatedEntity({
|
||||
operatorClient.handleIsolatedEntity({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
tableName: 'INVALID_TABLE_NAME',
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
getRangeOfValues,
|
||||
getRawRecordPairs,
|
||||
getUniqueRawsBy,
|
||||
hasSimilarUpdateAt,
|
||||
getValidRecordsForUpdate,
|
||||
retrieveRecords,
|
||||
} from '@database/operator/utils/general';
|
||||
import {
|
||||
|
|
@ -50,7 +50,7 @@ export interface BaseHandlerMix {
|
|||
activeDatabase: Database;
|
||||
getActiveDatabase: () => DatabaseInstance;
|
||||
setActiveDatabase: (database: Database) => void;
|
||||
handleIsolatedEntity: ({tableName, values, prepareRecordsOnly}: HandleIsolatedEntityArgs) => boolean | Model[];
|
||||
handleIsolatedEntity: ({tableName, values, prepareRecordsOnly}: HandleIsolatedEntityArgs) => Model[];
|
||||
handleEntityRecords: ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly}: HandleEntityRecordsArgs) => Promise<null | Model[]>;
|
||||
processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>;
|
||||
batchOperations: ({database, models}: BatchOperationsArgs) => Promise<void>;
|
||||
|
|
@ -68,7 +68,7 @@ class BaseHandler {
|
|||
*/
|
||||
activeDatabase: DatabaseInstance;
|
||||
|
||||
constructor(serverDatabase?: Database) {
|
||||
constructor(serverDatabase: Database) {
|
||||
this.activeDatabase = serverDatabase;
|
||||
}
|
||||
|
||||
|
|
@ -94,13 +94,14 @@ class BaseHandler {
|
|||
* @param {boolean} isolatedEntityArgs.prepareRecordsOnly
|
||||
* @param {RawValue} isolatedEntityArgs.values
|
||||
* @throws DataOperatorException
|
||||
* @returns {Model[] | boolean}
|
||||
* @returns {Model[]}
|
||||
*/
|
||||
handleIsolatedEntity = async ({tableName, values, prepareRecordsOnly = true}: HandleIsolatedEntityArgs) => {
|
||||
let findMatchingRecordBy;
|
||||
let fieldName;
|
||||
let operator;
|
||||
let rawValues;
|
||||
let records: Model[] = [];
|
||||
|
||||
if (!values.length) {
|
||||
throw new DataOperatorException(
|
||||
|
|
@ -146,9 +147,9 @@ class BaseHandler {
|
|||
}
|
||||
case IsolatedEntities.SYSTEM: {
|
||||
findMatchingRecordBy = isRecordSystemEqualToRaw;
|
||||
fieldName = 'id';
|
||||
fieldName = 'name';
|
||||
operator = prepareSystemRecord;
|
||||
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
|
||||
rawValues = getUniqueRawsBy({raws: values, key: 'name'});
|
||||
break;
|
||||
}
|
||||
case IsolatedEntities.TERMS_OF_SERVICE: {
|
||||
|
|
@ -166,7 +167,7 @@ class BaseHandler {
|
|||
}
|
||||
|
||||
if (fieldName && findMatchingRecordBy) {
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName,
|
||||
findMatchingRecordBy,
|
||||
operator,
|
||||
|
|
@ -175,10 +176,10 @@ class BaseHandler {
|
|||
tableName,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
}
|
||||
|
||||
return false;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -189,13 +190,14 @@ class BaseHandler {
|
|||
* @param {(DataFactoryArgs) => Promise<Model>} handleEntityArgs.operator
|
||||
* @param {RawValue[]} handleEntityArgs.rawValues
|
||||
* @param {string} handleEntityArgs.tableName
|
||||
* @returns {Promise<null | Model[]>}
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly = true}: HandleEntityRecordsArgs) => {
|
||||
if (!rawValues.length) {
|
||||
return null;
|
||||
throw new DataOperatorException(
|
||||
`An empty "rawValues" array has been passed to the handleEntityRecords method for tableName ${tableName}`,
|
||||
);
|
||||
}
|
||||
|
||||
const {createRaws, updateRaws} = await this.processInputs({
|
||||
rawValues,
|
||||
tableName,
|
||||
|
|
@ -205,7 +207,8 @@ class BaseHandler {
|
|||
|
||||
const database = await this.getDatabase(tableName);
|
||||
|
||||
const models = await this.prepareRecords({
|
||||
let models: Model[] = [];
|
||||
models = await this.prepareRecords({
|
||||
database,
|
||||
tableName,
|
||||
createRaws,
|
||||
|
|
@ -221,7 +224,7 @@ class BaseHandler {
|
|||
await this.batchOperations({database, models});
|
||||
}
|
||||
|
||||
return null;
|
||||
return models;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -263,23 +266,17 @@ class BaseHandler {
|
|||
const existingRecord = existingRecords[findIndex];
|
||||
|
||||
// Some raw value has an update_at field. We'll proceed to update only if the update_at value is different from the record's value in database
|
||||
const isUpdateAtSimilar = hasSimilarUpdateAt({
|
||||
const updateRecords = getValidRecordsForUpdate({
|
||||
tableName,
|
||||
existingRecord,
|
||||
newValue: newElement,
|
||||
});
|
||||
|
||||
if (!isUpdateAtSimilar) {
|
||||
return updateRaws.push({
|
||||
record: existingRecord,
|
||||
raw: newElement,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// This RawValue is not present in the database; hence, we need to create it
|
||||
return createRaws.push({record: undefined, raw: newElement});
|
||||
return updateRaws.push(updateRecords);
|
||||
}
|
||||
return null;
|
||||
|
||||
// This RawValue is not present in the database; hence, we need to create it
|
||||
return createRaws.push({record: undefined, raw: newElement});
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
@ -419,7 +416,8 @@ class BaseHandler {
|
|||
* @returns {Promise<Database>}
|
||||
*/
|
||||
getDefaultDatabase = async () => {
|
||||
const connection = await DatabaseManager.getDefaultDatabase();
|
||||
const databaseManagerClient = new DatabaseManager();
|
||||
const connection = await databaseManagerClient.getDefaultDatabase();
|
||||
if (connection === undefined) {
|
||||
throw new DatabaseConnectionException(
|
||||
'An error occurred while retrieving the default database',
|
||||
|
|
@ -440,16 +438,10 @@ class BaseHandler {
|
|||
return this.activeDatabase;
|
||||
}
|
||||
|
||||
// NOTE: here we are getting the active server directly as in a multi-server support system, the current
|
||||
// active server connection will already be set on application init
|
||||
const connection = await DatabaseManager.getActiveServerDatabase();
|
||||
if (connection === undefined) {
|
||||
throw new DatabaseConnectionException(
|
||||
'An error occurred while retrieving the server database',
|
||||
'',
|
||||
);
|
||||
}
|
||||
return connection;
|
||||
throw new DatabaseConnectionException(
|
||||
"This operator client didn't have its activeDatabase set",
|
||||
'',
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import Operator from '@database/operator';
|
||||
import {
|
||||
isRecordChannelEqualToRaw,
|
||||
isRecordChannelInfoEqualToRaw,
|
||||
|
|
@ -15,20 +16,39 @@ import {
|
|||
prepareMyChannelSettingsRecord,
|
||||
} from '@database/operator/prepareRecords/channel';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Operator: Channel Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleChannel: should write to CHANNEL entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleChannel({
|
||||
await operatorClient.handleChannel({
|
||||
channels: [
|
||||
{
|
||||
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
|
||||
|
|
@ -89,11 +109,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleMyChannelSettings({
|
||||
await operatorClient.handleMyChannelSettings({
|
||||
settings: [
|
||||
{
|
||||
channel_id: 'c',
|
||||
|
|
@ -138,11 +158,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleChannelInfo({
|
||||
await operatorClient.handleChannelInfo({
|
||||
channelInfos: [
|
||||
{
|
||||
channel_id: 'c',
|
||||
|
|
@ -180,11 +200,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
|||
it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleMyChannel({
|
||||
await operatorClient.handleMyChannel({
|
||||
myChannels: [
|
||||
{
|
||||
channel_id: 'c',
|
||||
|
|
|
|||
|
|
@ -48,9 +48,11 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawChannel[]} channelsArgs.channels
|
||||
* @param {boolean} channelsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Channel[]|boolean}
|
||||
* @returns {Channel[]}
|
||||
*/
|
||||
handleChannel = async ({channels, prepareRecordsOnly = true}: HandleChannelArgs) => {
|
||||
let records: Channel[] = [];
|
||||
|
||||
if (!channels.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "channels" array has been passed to the handleChannel method',
|
||||
|
|
@ -59,7 +61,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: channels, key: 'id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'id',
|
||||
findMatchingRecordBy: isRecordChannelEqualToRaw,
|
||||
operator: prepareChannelRecord,
|
||||
|
|
@ -68,7 +70,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
tableName: CHANNEL,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -77,9 +79,11 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawMyChannelSettings[]} settingsArgs.settings
|
||||
* @param {boolean} settingsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {MyChannelSettings[]| boolean}
|
||||
* @returns {MyChannelSettings[]}
|
||||
*/
|
||||
handleMyChannelSettings = async ({settings, prepareRecordsOnly = true}: HandleMyChannelSettingsArgs) => {
|
||||
let records: MyChannelSettings[] = [];
|
||||
|
||||
if (!settings.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "settings" array has been passed to the handleMyChannelSettings method',
|
||||
|
|
@ -88,7 +92,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'channel_id',
|
||||
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
|
||||
operator: prepareMyChannelSettingsRecord,
|
||||
|
|
@ -97,7 +101,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
tableName: MY_CHANNEL_SETTINGS,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -106,9 +110,11 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawChannelInfo[]} channelInfosArgs.channelInfos
|
||||
* @param {boolean} channelInfosArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {ChannelInfo[]| boolean}
|
||||
* @returns {ChannelInfo[]}
|
||||
*/
|
||||
handleChannelInfo = async ({channelInfos, prepareRecordsOnly = true}: HandleChannelInfoArgs) => {
|
||||
let records: ChannelInfo[] = [];
|
||||
|
||||
if (!channelInfos.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "channelInfos" array has been passed to the handleMyChannelSettings method',
|
||||
|
|
@ -120,7 +126,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
key: 'channel_id',
|
||||
});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'channel_id',
|
||||
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
|
||||
operator: prepareChannelInfoRecord,
|
||||
|
|
@ -129,7 +135,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
tableName: CHANNEL_INFO,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -138,9 +144,11 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawMyChannel[]} myChannelsArgs.myChannels
|
||||
* @param {boolean} myChannelsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {MyChannel[]| boolean}
|
||||
* @returns {MyChannel[]}
|
||||
*/
|
||||
handleMyChannel = async ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs) => {
|
||||
let records: MyChannel[] = [];
|
||||
|
||||
if (!myChannels.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "myChannels" array has been passed to the handleMyChannel method',
|
||||
|
|
@ -152,7 +160,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
key: 'channel_id',
|
||||
});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'channel_id',
|
||||
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
|
||||
operator: prepareMyChannelRecord,
|
||||
|
|
@ -161,7 +169,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
|||
tableName: MY_CHANNEL,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import Operator from '@database/operator';
|
||||
import {
|
||||
isRecordGroupEqualToRaw,
|
||||
isRecordGroupMembershipEqualToRaw,
|
||||
|
|
@ -15,20 +16,39 @@ import {
|
|||
prepareGroupsInTeamRecord,
|
||||
} from '@database/operator/prepareRecords/group';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Operator: Group Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleGroup: should write to GROUP entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleGroup({
|
||||
await operatorClient.handleGroup({
|
||||
groups: [
|
||||
{
|
||||
id: 'id_groupdfjdlfkjdkfdsf',
|
||||
|
|
@ -73,11 +93,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
|||
it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleGroupsInTeam({
|
||||
await operatorClient.handleGroupsInTeam({
|
||||
groupsInTeams: [
|
||||
{
|
||||
team_id: 'team_899',
|
||||
|
|
@ -118,11 +138,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
|||
it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleGroupsInChannel({
|
||||
await operatorClient.handleGroupsInChannel({
|
||||
groupsInChannels: [
|
||||
{
|
||||
auto_add: true,
|
||||
|
|
@ -173,11 +193,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
|||
it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleGroupMembership({
|
||||
await operatorClient.handleGroupMembership({
|
||||
groupMemberships: [
|
||||
{
|
||||
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
|
||||
|
|
|
|||
|
|
@ -48,9 +48,11 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
|
||||
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {GroupMembership[] | boolean}
|
||||
* @returns {GroupMembership[]}
|
||||
*/
|
||||
handleGroupMembership = async ({groupMemberships, prepareRecordsOnly = true}: HandleGroupMembershipArgs) => {
|
||||
let records: GroupMembership[] = [];
|
||||
|
||||
if (!groupMemberships.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "groupMemberships" array has been passed to the handleGroupMembership method',
|
||||
|
|
@ -59,7 +61,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'user_id',
|
||||
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
|
||||
operator: prepareGroupMembershipRecord,
|
||||
|
|
@ -68,7 +70,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
tableName: GROUP_MEMBERSHIP,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -77,9 +79,11 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawGroup[]} groupsArgs.groups
|
||||
* @param {boolean} groupsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Group[] | boolean}
|
||||
* @returns {Group[]}
|
||||
*/
|
||||
handleGroup = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs) => {
|
||||
let records: Group[] = [];
|
||||
|
||||
if (!groups.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "groups" array has been passed to the handleGroup method',
|
||||
|
|
@ -88,7 +92,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: groups, key: 'name'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'name',
|
||||
findMatchingRecordBy: isRecordGroupEqualToRaw,
|
||||
operator: prepareGroupRecord,
|
||||
|
|
@ -97,7 +101,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
tableName: GROUP,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -106,9 +110,11 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawGroupsInTeam[]} groupsInTeamsArgs.groupsInTeams
|
||||
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {GroupsInTeam[] | boolean}
|
||||
* @returns {GroupsInTeam[]}
|
||||
*/
|
||||
handleGroupsInTeam = async ({groupsInTeams, prepareRecordsOnly = true} : HandleGroupsInTeamArgs) => {
|
||||
let records: GroupsInTeam[] = [];
|
||||
|
||||
if (!groupsInTeams.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "groups" array has been passed to the handleGroupsInTeam method',
|
||||
|
|
@ -117,7 +123,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'group_id',
|
||||
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
|
||||
operator: prepareGroupsInTeamRecord,
|
||||
|
|
@ -126,7 +132,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
tableName: GROUPS_IN_TEAM,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -135,9 +141,11 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawGroupsInChannel[]} groupsInChannelsArgs.groupsInChannels
|
||||
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {GroupsInChannel[] | boolean}
|
||||
* @returns {GroupsInChannel[]}
|
||||
*/
|
||||
handleGroupsInChannel = async ({groupsInChannels, prepareRecordsOnly = true}: HandleGroupsInChannelArgs) => {
|
||||
let records: GroupsInChannel[] = [];
|
||||
|
||||
if (!groupsInChannels.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "groups" array has been passed to the handleGroupsInTeam method',
|
||||
|
|
@ -146,7 +154,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'group_id',
|
||||
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
|
||||
operator: prepareGroupsInChannelRecord,
|
||||
|
|
@ -155,7 +163,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
|||
tableName: GROUPS_IN_CHANNEL,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,48 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import Operator from '@database/operator';
|
||||
import {isRecordDraftEqualToRaw} from '@database/operator/comparators';
|
||||
import {prepareDraftRecord} from '@database/operator/prepareRecords/post';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Operator: Post Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleDraft: should write to the Draft entity', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
await createTestConnection({databaseName: 'post_handler', setActive: true});
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
const values = [
|
||||
{
|
||||
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
|
||||
files: [
|
||||
{
|
||||
id: '322dxx',
|
||||
user_id: 'user_id',
|
||||
post_id: 'post_id',
|
||||
create_at: 123,
|
||||
|
|
@ -42,7 +63,7 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
await DataOperator.handleDraft({drafts: values, prepareRecordsOnly: false});
|
||||
await operatorClient.handleDraft({drafts: values, prepareRecordsOnly: false});
|
||||
|
||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
||||
|
|
@ -206,17 +227,17 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const spyOnHandleFiles = jest.spyOn(DataOperator as any, 'handleFiles');
|
||||
const spyOnHandlePostMetadata = jest.spyOn(DataOperator as any, 'handlePostMetadata');
|
||||
const spyOnHandleReactions = jest.spyOn(DataOperator as any, 'handleReactions');
|
||||
const spyOnHandleCustomEmojis = jest.spyOn(DataOperator as any, 'handleIsolatedEntity');
|
||||
const spyOnHandlePostsInThread = jest.spyOn(DataOperator as any, 'handlePostsInThread');
|
||||
const spyOnHandlePostsInChannel = jest.spyOn(DataOperator as any, 'handlePostsInChannel');
|
||||
const spyOnHandleFiles = jest.spyOn(operatorClient as any, 'handleFiles');
|
||||
const spyOnHandlePostMetadata = jest.spyOn(operatorClient as any, 'handlePostMetadata');
|
||||
const spyOnHandleReactions = jest.spyOn(operatorClient as any, 'handleReactions');
|
||||
const spyOnHandleCustomEmojis = jest.spyOn(operatorClient as any, 'handleIsolatedEntity');
|
||||
const spyOnHandlePostsInThread = jest.spyOn(operatorClient as any, 'handlePostsInThread');
|
||||
const spyOnHandlePostsInChannel = jest.spyOn(operatorClient as any, 'handlePostsInChannel');
|
||||
|
||||
await createTestConnection({databaseName: 'post_handler', setActive: true});
|
||||
|
||||
// handlePosts will in turn call handlePostsInThread
|
||||
await DataOperator.handlePosts({
|
||||
await operatorClient.handlePosts({
|
||||
orders: [
|
||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
||||
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/comparators';
|
||||
|
|
@ -14,8 +17,6 @@ import {
|
|||
} from '@database/operator/prepareRecords/post';
|
||||
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
|
||||
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
import {
|
||||
HandleDraftArgs,
|
||||
HandleFilesArgs,
|
||||
|
|
@ -64,9 +65,11 @@ const PostHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawDraft[]} draftsArgs.drafts
|
||||
* @param {boolean} draftsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Draft[] | boolean}
|
||||
* @returns {Draft[]}
|
||||
*/
|
||||
handleDraft = async ({drafts, prepareRecordsOnly = true}: HandleDraftArgs) => {
|
||||
let records: Draft[] = [];
|
||||
|
||||
if (!drafts.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "drafts" array has been passed to the handleReactions method',
|
||||
|
|
@ -75,7 +78,7 @@ const PostHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'channel_id',
|
||||
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
||||
operator: prepareDraftRecord,
|
||||
|
|
@ -84,7 +87,7 @@ const PostHandler = (superclass: any) => class extends superclass {
|
|||
tableName: DRAFT,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import Operator from '@database/operator';
|
||||
import {
|
||||
isRecordMyTeamEqualToRaw,
|
||||
isRecordSlashCommandEqualToRaw,
|
||||
|
|
@ -19,20 +20,39 @@ import {
|
|||
prepareTeamSearchHistoryRecord,
|
||||
} from '@database/operator/prepareRecords/team';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Operator: Team Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleTeam: should write to TEAM entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleTeam({
|
||||
await operatorClient.handleTeam({
|
||||
teams: [
|
||||
{
|
||||
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
|
||||
|
|
@ -89,11 +109,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
|||
it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleTeamMemberships({
|
||||
await operatorClient.handleTeamMemberships({
|
||||
teamMemberships: [
|
||||
{
|
||||
team_id: 'a',
|
||||
|
|
@ -134,11 +154,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
|||
it('=> HandleMyTeam: should write to MY_TEAM entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleMyTeam({
|
||||
await operatorClient.handleMyTeam({
|
||||
myTeams: [
|
||||
{
|
||||
team_id: 'teamA',
|
||||
|
|
@ -171,11 +191,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
|||
it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleTeamChannelHistory({
|
||||
await operatorClient.handleTeamChannelHistory({
|
||||
teamChannelHistories: [
|
||||
{
|
||||
team_id: 'a',
|
||||
|
|
@ -199,11 +219,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
|||
it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleTeamSearchHistory({
|
||||
await operatorClient.handleTeamSearchHistory({
|
||||
teamSearchHistories: [
|
||||
{
|
||||
team_id: 'a',
|
||||
|
|
@ -236,11 +256,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
|||
it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleSlashCommand({
|
||||
await operatorClient.handleSlashCommand({
|
||||
slashCommands: [
|
||||
{
|
||||
id: 'command_1',
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import SlashCommand from '@typings/database/slash_command';
|
|||
import Team from '@typings/database/team';
|
||||
import TeamChannelHistory from '@typings/database/team_channel_history';
|
||||
import TeamMembership from '@typings/database/team_membership';
|
||||
import TeamSearchHistory from '@typings/database/team_search_history';
|
||||
|
||||
const {
|
||||
MY_TEAM,
|
||||
|
|
@ -44,11 +45,11 @@ const {
|
|||
} = MM_TABLES.SERVER;
|
||||
|
||||
export interface TeamHandlerMix {
|
||||
handleTeamMemberships : ({teamMemberships, prepareRecordsOnly}: HandleTeamMembershipArgs) => TeamMembership[] | boolean,
|
||||
handleTeam: ({teams, prepareRecordsOnly}: HandleTeamArgs) => Team[] | boolean
|
||||
handleTeamChannelHistory : ({teamChannelHistories, prepareRecordsOnly}: HandleTeamChannelHistoryArgs) => TeamChannelHistory[]| boolean,
|
||||
handleSlashCommand : ({slashCommands, prepareRecordsOnly} : HandleSlashCommandArgs) => SlashCommand[]| boolean,
|
||||
handleMyTeam : ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => MyTeam[]| boolean
|
||||
handleTeamMemberships: ({teamMemberships, prepareRecordsOnly}: HandleTeamMembershipArgs) => TeamMembership[];
|
||||
handleTeam: ({teams, prepareRecordsOnly}: HandleTeamArgs) => Team[];
|
||||
handleTeamChannelHistory: ({teamChannelHistories, prepareRecordsOnly}: HandleTeamChannelHistoryArgs) => TeamChannelHistory[];
|
||||
handleSlashCommand: ({slashCommands, prepareRecordsOnly}: HandleSlashCommandArgs) => SlashCommand[];
|
||||
handleMyTeam: ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => MyTeam[];
|
||||
}
|
||||
|
||||
const TeamHandler = (superclass: any) => class extends superclass {
|
||||
|
|
@ -58,9 +59,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawTeamMembership[]} teamMembershipsArgs.teamMemberships
|
||||
* @param {boolean} teamMembershipsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {TeamMembership[] | boolean}
|
||||
* @returns {TeamMembership[]}
|
||||
*/
|
||||
handleTeamMemberships = async ({teamMemberships, prepareRecordsOnly = true}: HandleTeamMembershipArgs) => {
|
||||
let records: TeamMembership[] = [];
|
||||
|
||||
if (!teamMemberships.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "teamMemberships" array has been passed to the handleTeamMemberships method',
|
||||
|
|
@ -69,7 +72,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'user_id',
|
||||
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
|
||||
operator: prepareTeamMembershipRecord,
|
||||
|
|
@ -78,7 +81,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
prepareRecordsOnly,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -87,9 +90,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawTeam[]} teamsArgs.teams
|
||||
* @param {boolean} teamsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Team[] | boolean}
|
||||
* @returns {Team[]}
|
||||
*/
|
||||
handleTeam = async ({teams, prepareRecordsOnly = true}: HandleTeamArgs) => {
|
||||
let records: Team[] = [];
|
||||
|
||||
if (!teams.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "teams" array has been passed to the handleTeam method',
|
||||
|
|
@ -98,7 +103,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: teams, key: 'id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'id',
|
||||
findMatchingRecordBy: isRecordTeamEqualToRaw,
|
||||
operator: prepareTeamRecord,
|
||||
|
|
@ -107,7 +112,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
tableName: TEAM,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -116,9 +121,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawTeamChannelHistory[]} teamChannelHistoriesArgs.teamChannelHistories
|
||||
* @param {boolean} teamChannelHistoriesArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {TeamChannelHistory[]| boolean}
|
||||
* @returns {TeamChannelHistory[]}
|
||||
*/
|
||||
handleTeamChannelHistory = async ({teamChannelHistories, prepareRecordsOnly = true}: HandleTeamChannelHistoryArgs) => {
|
||||
let records: TeamChannelHistory[] = [];
|
||||
|
||||
if (!teamChannelHistories.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "teamChannelHistories" array has been passed to the handleTeamChannelHistory method',
|
||||
|
|
@ -127,7 +134,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'team_id',
|
||||
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
|
||||
operator: prepareTeamChannelHistoryRecord,
|
||||
|
|
@ -136,7 +143,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
tableName: TEAM_CHANNEL_HISTORY,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -145,9 +152,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawTeamSearchHistory[]} teamSearchHistoriesArgs.teamSearchHistories
|
||||
* @param {boolean} teamSearchHistoriesArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {TeamSearchHistory[]| boolean}
|
||||
* @returns {TeamSearchHistory[]}
|
||||
*/
|
||||
handleTeamSearchHistory = async ({teamSearchHistories, prepareRecordsOnly = true}: HandleTeamSearchHistoryArgs) => {
|
||||
let records: TeamSearchHistory[] = [];
|
||||
|
||||
if (!teamSearchHistories.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "teamSearchHistories" array has been passed to the handleTeamSearchHistory method',
|
||||
|
|
@ -156,7 +165,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'team_id',
|
||||
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
|
||||
operator: prepareTeamSearchHistoryRecord,
|
||||
|
|
@ -165,7 +174,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
tableName: TEAM_SEARCH_HISTORY,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -174,9 +183,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawSlashCommand[]} slashCommandsArgs.slashCommands
|
||||
* @param {boolean} slashCommandsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {SlashCommand[]| boolean}
|
||||
* @returns {SlashCommand[]}
|
||||
*/
|
||||
handleSlashCommand = async ({slashCommands, prepareRecordsOnly = true} : HandleSlashCommandArgs) => {
|
||||
handleSlashCommand = async ({slashCommands, prepareRecordsOnly = true}: HandleSlashCommandArgs) => {
|
||||
let records: SlashCommand[] = [];
|
||||
|
||||
if (!slashCommands.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "slashCommands" array has been passed to the handleSlashCommand method',
|
||||
|
|
@ -185,7 +196,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'id',
|
||||
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
|
||||
operator: prepareSlashCommandRecord,
|
||||
|
|
@ -194,7 +205,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
tableName: SLASH_COMMAND,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -203,9 +214,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawMyTeam[]} myTeamsArgs.myTeams
|
||||
* @param {boolean} myTeamsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {MyTeam[]| boolean}
|
||||
* @returns {MyTeam[]}
|
||||
*/
|
||||
handleMyTeam = async ({myTeams, prepareRecordsOnly = true}: HandleMyTeamArgs) => {
|
||||
let records: MyTeam[] = [];
|
||||
|
||||
if (!myTeams.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "myTeams" array has been passed to the handleSlashCommand method',
|
||||
|
|
@ -214,7 +227,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'team_id',
|
||||
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
|
||||
operator: prepareMyTeamRecord,
|
||||
|
|
@ -223,7 +236,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
|||
tableName: MY_TEAM,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import Operator from '@database/operator';
|
||||
import {
|
||||
isRecordChannelMembershipEqualToRaw,
|
||||
isRecordPreferenceEqualToRaw,
|
||||
|
|
@ -13,21 +14,40 @@ import {
|
|||
prepareUserRecord,
|
||||
} from '@database/operator/prepareRecords/user';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** Operator: User Handlers tests ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
let operatorClient: Operator;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'base_handler',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl: 'baseHandler.test.com',
|
||||
},
|
||||
});
|
||||
|
||||
operatorClient = new Operator(database!);
|
||||
});
|
||||
|
||||
it('=> HandleReactions: should write to both Reactions and CustomEmoji entities', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||
|
||||
const spyOnPrepareRecords = jest.spyOn(DataOperator as any, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(DataOperator as any, 'batchOperations');
|
||||
const spyOnPrepareRecords = jest.spyOn(operatorClient as any, 'prepareRecords');
|
||||
const spyOnBatchOperation = jest.spyOn(operatorClient as any, 'batchOperations');
|
||||
|
||||
await DataOperator.handleReactions({
|
||||
await operatorClient.handleReactions({
|
||||
reactions: [
|
||||
{
|
||||
create_at: 1608263728086,
|
||||
|
|
@ -88,16 +108,16 @@ describe('*** Operator: User Handlers tests ***', () => {
|
|||
timezone: {
|
||||
automaticTimezone: 'Indian/Mauritius',
|
||||
manualTimezone: '',
|
||||
useAutomaticTimezone: true,
|
||||
useAutomaticTimezone: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleUsers({users, prepareRecordsOnly: false});
|
||||
await operatorClient.handleUsers({users, prepareRecordsOnly: false});
|
||||
|
||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
|
||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||
|
|
@ -139,7 +159,7 @@ describe('*** Operator: User Handlers tests ***', () => {
|
|||
timezone: {
|
||||
automaticTimezone: 'Indian/Mauritius',
|
||||
manualTimezone: '',
|
||||
useAutomaticTimezone: true,
|
||||
useAutomaticTimezone: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -153,11 +173,11 @@ describe('*** Operator: User Handlers tests ***', () => {
|
|||
it('=> HandlePreferences: should write to PREFERENCE entity', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||
|
||||
await DataOperator.handlePreferences({
|
||||
await operatorClient.handlePreferences({
|
||||
preferences: [
|
||||
{
|
||||
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
|
||||
|
|
@ -269,11 +289,11 @@ describe('*** Operator: User Handlers tests ***', () => {
|
|||
},
|
||||
];
|
||||
|
||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
||||
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||
|
||||
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||
|
||||
await DataOperator.handleChannelMembership({
|
||||
await operatorClient.handleChannelMembership({
|
||||
channelMemberships,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
} from '@database/operator/prepareRecords/user';
|
||||
import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general';
|
||||
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
import ChannelMembership from '@typings/database/channel_membership';
|
||||
import CustomEmoji from '@typings/database/custom_emoji';
|
||||
import {
|
||||
|
|
@ -40,10 +39,10 @@ const {
|
|||
} = MM_TABLES.SERVER;
|
||||
|
||||
export interface UserHandlerMix {
|
||||
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[] | boolean,
|
||||
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[] | boolean,
|
||||
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => boolean | (Reaction | CustomEmoji)[],
|
||||
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[] | boolean
|
||||
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[];
|
||||
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[];
|
||||
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) =>(Reaction | CustomEmoji)[];
|
||||
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[];
|
||||
}
|
||||
|
||||
const UserHandler = (superclass: any) => class extends superclass {
|
||||
|
|
@ -53,21 +52,20 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawChannelMembership[]} channelMembershipsArgs.channelMemberships
|
||||
* @param {boolean} channelMembershipsArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {ChannelMembership[] | boolean}
|
||||
* @returns {ChannelMembership[]}
|
||||
*/
|
||||
handleChannelMembership = async ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs) => {
|
||||
let records: ChannelMembership[] = [];
|
||||
|
||||
if (!channelMemberships.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "channelMemberships" array has been passed to the handleChannelMembership method',
|
||||
);
|
||||
}
|
||||
|
||||
const rawValues = getUniqueRawsBy({
|
||||
raws: channelMemberships,
|
||||
key: 'channel_id',
|
||||
});
|
||||
const rawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'user_id',
|
||||
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
|
||||
operator: prepareChannelMembershipRecord,
|
||||
|
|
@ -76,7 +74,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
tableName: CHANNEL_MEMBERSHIP,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -85,9 +83,11 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawPreference[]} preferencesArgs.preferences
|
||||
* @param {boolean} preferencesArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {Preference[] | boolean}
|
||||
* @returns {Preference[]}
|
||||
*/
|
||||
handlePreferences = async ({preferences, prepareRecordsOnly = true}: HandlePreferencesArgs) => {
|
||||
let records: Preference[] = [];
|
||||
|
||||
if (!preferences.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "preferences" array has been passed to the handlePreferences method',
|
||||
|
|
@ -96,7 +96,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'user_id',
|
||||
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
|
||||
operator: preparePreferenceRecord,
|
||||
|
|
@ -105,7 +105,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
tableName: PREFERENCE,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -114,9 +114,11 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawReaction[]} handleReactions.reactions
|
||||
* @param {boolean} handleReactions.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {boolean | (Reaction | CustomEmoji)[]}
|
||||
* @returns {(Reaction| CustomEmoji)[]}
|
||||
*/
|
||||
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => {
|
||||
let batchRecords: (Reaction| CustomEmoji)[] = [];
|
||||
|
||||
if (!reactions.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "reactions" array has been passed to the handleReactions method',
|
||||
|
|
@ -137,8 +139,6 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
rawReactions: rawValues,
|
||||
});
|
||||
|
||||
let batchRecords: Model[] = [];
|
||||
|
||||
if (createReactions.length) {
|
||||
// Prepares record for model Reactions
|
||||
const reactionsRecords = (await this.prepareRecords({
|
||||
|
|
@ -174,7 +174,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -183,9 +183,11 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
* @param {RawUser[]} usersArgs.users
|
||||
* @param {boolean} usersArgs.prepareRecordsOnly
|
||||
* @throws DataOperatorException
|
||||
* @returns {User[] | boolean}
|
||||
* @returns {User[]}
|
||||
*/
|
||||
handleUsers = async ({users, prepareRecordsOnly = true}: HandleUsersArgs) => {
|
||||
let records: User[] = [];
|
||||
|
||||
if (!users.length) {
|
||||
throw new DataOperatorException(
|
||||
'An empty "users" array has been passed to the handleUsers method',
|
||||
|
|
@ -194,7 +196,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
|
||||
const rawValues = getUniqueRawsBy({raws: users, key: 'id'});
|
||||
|
||||
const records = await this.handleEntityRecords({
|
||||
records = await this.handleEntityRecords({
|
||||
fieldName: 'id',
|
||||
findMatchingRecordBy: isRecordUserEqualToRaw,
|
||||
operator: prepareUserRecord,
|
||||
|
|
@ -203,7 +205,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
|||
prepareRecordsOnly,
|
||||
});
|
||||
|
||||
return prepareRecordsOnly && records?.length && records;
|
||||
return records;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,18 +7,20 @@ import GroupHandler, {GroupHandlerMix} from '@database/operator/handlers/group';
|
|||
import PostHandler, {PostHandlerMix} from '@database/operator/handlers/post';
|
||||
import TeamHandler, {TeamHandlerMix} from '@database/operator/handlers/team';
|
||||
import UserHandler, {UserHandlerMix} from '@database/operator/handlers/user';
|
||||
import {DatabaseInstance} from '@typings/database/database';
|
||||
import mix from '@utils/mix';
|
||||
|
||||
interface Operator extends BaseHandlerMix, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
|
||||
|
||||
class Operator extends mix(BaseHandler).with(
|
||||
PostHandler,
|
||||
UserHandler,
|
||||
GroupHandler,
|
||||
ChannelHandler,
|
||||
TeamHandler,
|
||||
) {}
|
||||
class Operator extends mix(BaseHandler).with(PostHandler, UserHandler, GroupHandler, ChannelHandler, TeamHandler) {
|
||||
database: DatabaseInstance;
|
||||
|
||||
const DataOperator = new Operator();
|
||||
constructor(activeDatabase?: DatabaseInstance) {
|
||||
super();
|
||||
if (activeDatabase) {
|
||||
this.activeDatabase = activeDatabase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {DataOperator, Operator};
|
||||
export default Operator;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {
|
||||
prepareAppRecord,
|
||||
prepareCustomEmojiRecord,
|
||||
|
|
@ -11,14 +12,21 @@ import {
|
|||
prepareTermsOfServiceRecord,
|
||||
} from '@database/operator/prepareRecords/general';
|
||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {OperationType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
||||
describe('*** Isolated Prepare Records Test ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
});
|
||||
|
||||
it('=> prepareAppRecord: should return an array of type App', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
const database = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await prepareAppRecord({
|
||||
|
|
@ -41,7 +49,7 @@ describe('*** Isolated Prepare Records Test ***', () => {
|
|||
it('=> prepareGlobalRecord: should return an array of type Global', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
const database = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await prepareGlobalRecord({
|
||||
|
|
@ -60,7 +68,7 @@ describe('*** Isolated Prepare Records Test ***', () => {
|
|||
it('=> prepareServersRecord: should return an array of type Servers', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const database = await DatabaseManager.getDefaultDatabase();
|
||||
const database = await databaseManagerClient.getDefaultDatabase();
|
||||
expect(database).toBeTruthy();
|
||||
|
||||
const preparedRecords = await prepareServersRecord({
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export const preparePreferenceRecord = ({action, database, value}: DataFactoryAr
|
|||
|
||||
// id of preference comes from server response
|
||||
const generator = (preference: Preference) => {
|
||||
preference._raw.id = isCreateAction ? (raw?.id ?? preference.id) : record.id;
|
||||
preference._raw.id = isCreateAction ? preference.id : record.id;
|
||||
preference.category = raw.category;
|
||||
preference.name = raw.name;
|
||||
preference.userId = raw.user_id;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ jest.mock('@database/manager');
|
|||
|
||||
export const createTestConnection = async ({databaseName = 'db_name', setActive = false}) => {
|
||||
const serverUrl = 'https://appv2.mattermost.com';
|
||||
const database = await DatabaseManager.createDatabaseConnection({
|
||||
const databaseClient = new DatabaseManager();
|
||||
const database = await databaseClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -20,10 +21,7 @@ export const createTestConnection = async ({databaseName = 'db_name', setActive
|
|||
});
|
||||
|
||||
if (setActive) {
|
||||
await DatabaseManager.setActiveServerDatabase({
|
||||
displayName: databaseName,
|
||||
serverUrl,
|
||||
});
|
||||
await databaseClient.setActiveServerDatabase(serverUrl);
|
||||
}
|
||||
|
||||
return database;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import User from '@typings/database/user';
|
|||
const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
|
||||
|
||||
/**
|
||||
* hasSimilarUpdateAt: Database Operations on some entities are expensive. As such, we would like to operate if and only if we are
|
||||
* getValidRecordsForUpdate: Database Operations on some entities are expensive. As such, we would like to operate if and only if we are
|
||||
* 100% sure that the records are actually different from what we already have in the database.
|
||||
* @param {IdenticalRecordArgs} identicalRecord
|
||||
* @param {string} identicalRecord.tableName
|
||||
|
|
@ -31,16 +31,26 @@ const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
|
|||
* @param {Model} identicalRecord.existingRecord
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const hasSimilarUpdateAt = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
|
||||
export const getValidRecordsForUpdate = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
|
||||
const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER];
|
||||
|
||||
if (guardTables.includes(tableName)) {
|
||||
type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel;
|
||||
type ExistingRecord = Post | User | Team | SlashCommand | Channel;
|
||||
|
||||
return (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt;
|
||||
const shouldUpdate = (newValue as Raw).update_at === (existingRecord as ExistingRecord).updateAt;
|
||||
|
||||
if (shouldUpdate) {
|
||||
return {
|
||||
record: existingRecord,
|
||||
raw: newValue,
|
||||
};
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
return {
|
||||
record: existingRecord,
|
||||
raw: newValue,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -90,7 +100,7 @@ export const getUniqueRawsBy = ({raws, key}:{ raws: RawValue[], key: string}) =>
|
|||
* @param {RetrieveRecordsArgs} records
|
||||
* @param {Database} records.database
|
||||
* @param {string} records.tableName
|
||||
* @param {any} records.condition
|
||||
* @param {Clause} records.condition
|
||||
* @returns {Promise<Model[]>}
|
||||
*/
|
||||
export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DataOperator} from '@database/operator';
|
||||
import Operator from '@database/operator';
|
||||
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
|
||||
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -71,7 +71,8 @@ describe('DataOperator: Utils tests', () => {
|
|||
it('=> sanitizeReactions: should triage between reactions that needs creation/deletion and emojis to be created', async () => {
|
||||
const dbName = 'server_schema_connection';
|
||||
const serverUrl = 'https://appv2.mattermost.com';
|
||||
const database = await DatabaseManager.createDatabaseConnection({
|
||||
const databaseManagerClient = new DatabaseManager();
|
||||
const database = await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
@ -80,13 +81,12 @@ describe('DataOperator: Utils tests', () => {
|
|||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase({
|
||||
displayName: dbName,
|
||||
serverUrl,
|
||||
});
|
||||
await databaseManagerClient.setActiveServerDatabase(serverUrl);
|
||||
|
||||
const operatorClient = new Operator(database!);
|
||||
|
||||
// we commit one Reaction to our database
|
||||
const prepareRecords = await DataOperator.handleReactions({
|
||||
const prepareRecords = await operatorClient.handleReactions({
|
||||
reactions: [
|
||||
{
|
||||
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
|
||||
|
|
|
|||
|
|
@ -1,38 +1,19 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {Operator} from '@database/operator/index';
|
||||
import Operator from '@database/operator/index';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
|
||||
|
||||
export const createDataOperator = async (serverUrl: string) => {
|
||||
// Retrieves the connection matching serverUrl
|
||||
const connections = await DatabaseManager.retrieveDatabaseInstances([
|
||||
serverUrl,
|
||||
]);
|
||||
const databaseManagerClient = new DatabaseManager();
|
||||
|
||||
if (connections?.length) {
|
||||
// finds the connection that corresponds to the serverUrl value
|
||||
const index = connections.findIndex((databaseInstance) => {
|
||||
return databaseInstance.url === serverUrl;
|
||||
});
|
||||
|
||||
if (!connections?.[index]?.dbInstance) {
|
||||
throw new DatabaseConnectionException(
|
||||
`An instance of a database connection was found but we could not create a connection out of it for url: ${serverUrl}`,
|
||||
);
|
||||
}
|
||||
|
||||
const connection = connections[index].dbInstance as Database;
|
||||
|
||||
const operator = new Operator();
|
||||
operator.setActiveDatabase(connection);
|
||||
const connection = await databaseManagerClient.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
|
||||
|
||||
if (connection) {
|
||||
const operator = new Operator(connection);
|
||||
return operator;
|
||||
}
|
||||
|
||||
throw new DatabaseConnectionException(
|
||||
`No database has been registered with this url: ${serverUrl}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {createDataOperator} from '@database/operator/wrapper/index';
|
||||
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
|
||||
import {DatabaseType} from '@typings/database/enums';
|
||||
|
||||
jest.mock('@database/manager');
|
||||
|
|
@ -11,13 +10,19 @@ jest.mock('@database/manager');
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
describe('*** DataOperator Wrapper ***', () => {
|
||||
let databaseManagerClient: DatabaseManager;
|
||||
|
||||
beforeAll(async () => {
|
||||
databaseManagerClient = new DatabaseManager();
|
||||
});
|
||||
|
||||
it('=> wrapper should return an instance of DataOperator ', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const serverUrl = 'https://wrapper.mattermost.com';
|
||||
|
||||
// first we create the connection and save it into default database
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
await databaseManagerClient.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'community mattermost',
|
||||
|
|
@ -32,29 +37,6 @@ describe('*** DataOperator Wrapper ***', () => {
|
|||
expect(dataOperator).toBeTruthy();
|
||||
});
|
||||
|
||||
it('=> wrapper should throw an error due to invalid server url', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const serverUrl = 'https://wrapper.mattermost.com';
|
||||
|
||||
// first we create the connection and save it into default database
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
dbName: 'test_database',
|
||||
dbType: DatabaseType.SERVER,
|
||||
serverUrl,
|
||||
},
|
||||
shouldAddToDefaultDatabase: true,
|
||||
});
|
||||
|
||||
await expect(createDataOperator('https://error.com')).rejects.toThrow(
|
||||
'No database has been registered with this url: https://error.com',
|
||||
);
|
||||
|
||||
await expect(createDataOperator('https://error.com')).rejects.toThrow(DatabaseConnectionException);
|
||||
});
|
||||
|
||||
it('=> wrapper to handlePosts [OTHER DATABASE]: should write to Post and its sub-child entities', async () => {
|
||||
expect.assertions(12);
|
||||
|
||||
|
|
@ -212,7 +194,7 @@ describe('*** DataOperator Wrapper ***', () => {
|
|||
];
|
||||
|
||||
// create connection to other server in default db
|
||||
await DatabaseManager.createDatabaseConnection({
|
||||
await databaseManagerClient.createDatabaseConnection({
|
||||
shouldAddToDefaultDatabase: true,
|
||||
configs: {
|
||||
actionsEnabled: true,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export default tableSchema({
|
|||
{name: 'roles', type: 'string'},
|
||||
{name: 'status', type: 'string'},
|
||||
{name: 'timezone', type: 'string'},
|
||||
{name: 'user_id', type: 'string'},
|
||||
{name: 'username', type: 'string'},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -492,7 +492,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
roles: {name: 'roles', type: 'string'},
|
||||
status: {name: 'status', type: 'string'},
|
||||
timezone: {name: 'timezone', type: 'string'},
|
||||
user_id: {name: 'user_id', type: 'string'},
|
||||
username: {name: 'username', type: 'string'},
|
||||
},
|
||||
columnArray: [
|
||||
|
|
@ -513,7 +512,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
|||
{name: 'roles', type: 'string'},
|
||||
{name: 'status', type: 'string'},
|
||||
{name: 'timezone', type: 'string'},
|
||||
{name: 'user_id', type: 'string'},
|
||||
{name: 'username', type: 'string'},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,82 +2,172 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment';
|
||||
import {Platform} from 'react-native';
|
||||
import {getLocales} from 'react-native-localize';
|
||||
|
||||
import en from '@assets/i18n/en.json';
|
||||
|
||||
export const DEFAULT_LOCALE = 'en';
|
||||
const deviceLocale = getLocales()[0].languageCode;
|
||||
export const DEFAULT_LOCALE = deviceLocale;
|
||||
|
||||
function loadTranslation(locale: string) {
|
||||
function loadTranslation(locale?: string) {
|
||||
try {
|
||||
let translations;
|
||||
let momentData;
|
||||
|
||||
switch (locale) {
|
||||
case 'de':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/de');
|
||||
require('@formatjs/intl-numberformat/locale-data/de');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/de');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/de.json');
|
||||
momentData = require('moment/locale/de');
|
||||
break;
|
||||
case 'es':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/es');
|
||||
require('@formatjs/intl-numberformat/locale-data/es');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/es');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/es.json');
|
||||
momentData = require('moment/locale/es');
|
||||
break;
|
||||
case 'fr':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/fr');
|
||||
require('@formatjs/intl-numberformat/locale-data/fr');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/fr');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/fr.json');
|
||||
momentData = require('moment/locale/fr');
|
||||
break;
|
||||
case 'it':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/it');
|
||||
require('@formatjs/intl-numberformat/locale-data/it');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/it');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/it.json');
|
||||
momentData = require('moment/locale/it');
|
||||
break;
|
||||
case 'ja':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/ja');
|
||||
require('@formatjs/intl-numberformat/locale-data/ja');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ja');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/ja.json');
|
||||
momentData = require('moment/locale/ja');
|
||||
break;
|
||||
case 'ko':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/ko');
|
||||
require('@formatjs/intl-numberformat/locale-data/ko');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ko');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/ko.json');
|
||||
momentData = require('moment/locale/ko');
|
||||
break;
|
||||
case 'nl':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/nl');
|
||||
require('@formatjs/intl-numberformat/locale-data/nl');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/nl');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/nl.json');
|
||||
momentData = require('moment/locale/nl');
|
||||
break;
|
||||
case 'pl':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/pl');
|
||||
require('@formatjs/intl-numberformat/locale-data/pl');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/pl');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/pl.json');
|
||||
momentData = require('moment/locale/pl');
|
||||
break;
|
||||
case 'pt-BR':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/pt');
|
||||
require('@formatjs/intl-numberformat/locale-data/pt');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/pt');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/pt-BR.json');
|
||||
momentData = require('moment/locale/pt-br');
|
||||
break;
|
||||
case 'ro':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/ro');
|
||||
require('@formatjs/intl-numberformat/locale-data/ro');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ro');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/ro.json');
|
||||
momentData = require('moment/locale/ro');
|
||||
break;
|
||||
case 'ru':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/ru');
|
||||
require('@formatjs/intl-numberformat/locale-data/ru');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/ru');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/ru.json');
|
||||
momentData = require('moment/locale/ru');
|
||||
break;
|
||||
case 'tr':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/tr');
|
||||
require('@formatjs/intl-numberformat/locale-data/tr');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/tr');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/tr.json');
|
||||
momentData = require('moment/locale/tr');
|
||||
break;
|
||||
case 'uk':
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/uk');
|
||||
require('@formatjs/intl-numberformat/locale-data/uk');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/uk');
|
||||
}
|
||||
|
||||
translations = require('@assets/i18n/uk.json');
|
||||
momentData = require('moment/locale/uk');
|
||||
break;
|
||||
case 'zh-CN':
|
||||
loadChinesePolyfills();
|
||||
translations = require('@assets/i18n/zh-CN.json');
|
||||
momentData = require('moment/locale/zh-cn');
|
||||
break;
|
||||
case 'zh-TW':
|
||||
loadChinesePolyfills();
|
||||
translations = require('@assets/i18n/zh-TW.json');
|
||||
momentData = require('moment/locale/zh-tw');
|
||||
break;
|
||||
default:
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/en');
|
||||
require('@formatjs/intl-numberformat/locale-data/en');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/en');
|
||||
}
|
||||
|
||||
translations = en;
|
||||
break;
|
||||
}
|
||||
|
||||
if (momentData) {
|
||||
if (momentData && locale) {
|
||||
moment.updateLocale(locale.toLowerCase(), momentData);
|
||||
} else {
|
||||
resetMomentLocale();
|
||||
|
|
@ -89,11 +179,19 @@ function loadTranslation(locale: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function loadChinesePolyfills() {
|
||||
if (Platform.OS === 'android') {
|
||||
require('@formatjs/intl-pluralrules/locale-data/zh');
|
||||
require('@formatjs/intl-numberformat/locale-data/zh');
|
||||
require('@formatjs/intl-datetimeformat/locale-data/zh');
|
||||
}
|
||||
}
|
||||
|
||||
export function resetMomentLocale() {
|
||||
moment.locale(DEFAULT_LOCALE);
|
||||
}
|
||||
|
||||
export function getTranslations(locale: string) {
|
||||
export function getTranslations(locale?: string) {
|
||||
return loadTranslation(locale);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ const isSystemAdmin = (roles: string) => {
|
|||
|
||||
const clientMap: Record<string, Analytics> = {};
|
||||
|
||||
class Analytics {
|
||||
export class Analytics {
|
||||
analytics: RudderClient | null = null;
|
||||
context: any;
|
||||
diagnosticId: string | undefined;
|
||||
|
|
|
|||
|
|
@ -1,90 +1,112 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import * as KeyChain from 'react-native-keychain';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import * as analytics from '@init/analytics';
|
||||
import emmProvider from '@init/emm_provider';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
import type {ServerCredentials} from '@typings/credentials';
|
||||
|
||||
const ASYNC_STORAGE_CURRENT_SERVER_KEY = '@currentServerUrl';
|
||||
|
||||
export const getCurrentServerUrl = async () => {
|
||||
// TODO: Use default database to retrieve the current server url
|
||||
// and fallback to AsyncStorage if needed
|
||||
// TODO: This function is only necessary to support pre-Gekidou
|
||||
// versions as the active server URL may be stored in AsyncStorage.
|
||||
// At some point we can remove this function and rely solely on
|
||||
// the database manager's `getActiveServerUrl`.
|
||||
export const getActiveServerUrl = async () => {
|
||||
let serverUrl: string | null | undefined;
|
||||
|
||||
const databaseManager = new DatabaseManager();
|
||||
serverUrl = await databaseManager.getActiveServerUrl(); // TODO: need funciton to get active server url
|
||||
if (!serverUrl) {
|
||||
// If upgrading from non-Gekidou, the server URL might be in
|
||||
// AsyncStorage. If so, retrieve the server URL, create a DB for it,
|
||||
// then delete the AsyncStorage item.
|
||||
serverUrl = await AsyncStorage.getItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
|
||||
if (serverUrl) {
|
||||
databaseManager.setActiveServerDatabase(serverUrl);
|
||||
AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
const serverUrl = await AsyncStorage.getItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
|
||||
EphemeralStore.currentServerUrl = serverUrl;
|
||||
return serverUrl;
|
||||
};
|
||||
|
||||
export const setAppCredentials = (deviceToken: string, currentUserId: string, token: string, url: string) => {
|
||||
if (!currentUserId) {
|
||||
export const setServerCredentials = (serverUrl: string, userId: string, token: string) => {
|
||||
if (!(serverUrl && userId && token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only save to keychain if the url and token are set
|
||||
if (url && token) {
|
||||
try {
|
||||
const username = `${deviceToken}, ${currentUserId}`;
|
||||
const accessGroup = emmProvider.getAppGroupIdentifier();
|
||||
|
||||
EphemeralStore.deviceToken = deviceToken;
|
||||
EphemeralStore.currentServerUrl = url;
|
||||
AsyncStorage.setItem(ASYNC_STORAGE_CURRENT_SERVER_KEY, url);
|
||||
const options: KeyChain.Options = {
|
||||
accessGroup,
|
||||
securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE,
|
||||
};
|
||||
KeyChain.setInternetCredentials(url, username, token, options);
|
||||
} catch (e) {
|
||||
console.warn('could not set credentials', e); //eslint-disable-line no-console
|
||||
try {
|
||||
let accessGroup;
|
||||
if (Platform.OS === 'ios') {
|
||||
const appGroup = getIOSAppGroupDetails();
|
||||
accessGroup = appGroup.appGroupIdentifier;
|
||||
}
|
||||
|
||||
const options: KeyChain.Options = {
|
||||
accessGroup,
|
||||
securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE,
|
||||
};
|
||||
KeyChain.setInternetCredentials(serverUrl, userId, token, options);
|
||||
} catch (e) {
|
||||
console.warn('could not set credentials', e); //eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppCredentials = async () => {
|
||||
const serverUrl = await getCurrentServerUrl();
|
||||
export const getActiveServerCredentials = async () => {
|
||||
const serverUrl = await getActiveServerUrl();
|
||||
if (serverUrl) {
|
||||
return getServerCredentials(serverUrl);
|
||||
}
|
||||
|
||||
return getInternetCredentials(serverUrl || '');
|
||||
return null;
|
||||
};
|
||||
|
||||
export const removeAppCredentials = async () => {
|
||||
const url = await getCurrentServerUrl();
|
||||
|
||||
export const removeServerCredentials = async (serverUrl: string) => {
|
||||
// TODO: invalidate client and remove tokens
|
||||
|
||||
if (url) {
|
||||
KeyChain.resetInternetCredentials(url);
|
||||
}
|
||||
KeyChain.resetInternetCredentials(serverUrl);
|
||||
|
||||
EphemeralStore.currentServerUrl = null;
|
||||
AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
|
||||
};
|
||||
|
||||
export async function getInternetCredentials(url: string) {
|
||||
export const removeActiveServerCredentials = async () => {
|
||||
const serverUrl = await getActiveServerUrl();
|
||||
if (serverUrl) {
|
||||
removeServerCredentials(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
export const getServerCredentials = async (serverUrl: string): Promise<ServerCredentials|null> => {
|
||||
try {
|
||||
const credentials = await KeyChain.getInternetCredentials(url);
|
||||
const credentials = await KeyChain.getInternetCredentials(serverUrl);
|
||||
|
||||
if (credentials) {
|
||||
const usernameParsed = credentials.username.split(',');
|
||||
// TODO: Pre-Gekidou we were concatenating the deviceToken and the userId in
|
||||
// credentials.username so we need to check the length of credentials.username.split(',').
|
||||
// This check should be removed at some point.
|
||||
const parts = credentials.username.split(',');
|
||||
const userId = parts[parts.length - 1];
|
||||
|
||||
const token = credentials.password;
|
||||
const [deviceToken, currentUserId] = usernameParsed;
|
||||
|
||||
if (token && token !== 'undefined') {
|
||||
EphemeralStore.deviceToken = deviceToken;
|
||||
const analyticsClient = analytics.get(url);
|
||||
analyticsClient?.setUserId(currentUserId);
|
||||
const analyticsClient = analytics.get(serverUrl);
|
||||
analyticsClient?.setUserId(userId);
|
||||
|
||||
const csrf = await getCSRFFromCookie(url);
|
||||
const csrf = await getCSRFFromCookie(serverUrl);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('CSRF', csrf);
|
||||
|
||||
// TODO: Create client and set token / CSRF
|
||||
|
||||
return credentials;
|
||||
return {userId, token};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,4 +114,4 @@ export async function getInternetCredentials(url: string) {
|
|||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Emm from '@mattermost/react-native-emm';
|
||||
import {Alert, AlertButton, AppState, AppStateStatus, NativeModules, Platform} from 'react-native';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
|
||||
import {getAppCredentials} from './credentials';
|
||||
|
||||
const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 1000;
|
||||
|
||||
class EMMProvider {
|
||||
allowOtherServers = true;
|
||||
appGroupIdentifier: string | undefined;
|
||||
backgroundSince = 0;
|
||||
blurApplicationScreen = false;
|
||||
enabled = false;
|
||||
inAppPinCode = false;
|
||||
jailbreakProtection = false;
|
||||
performingAuthentication = false;
|
||||
previousAppState: AppStateStatus | undefined;
|
||||
vendor = 'Mattermost';
|
||||
|
||||
constructor() {
|
||||
Emm.addListener(this.handleManagedConfig);
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
const {MattermostManaged} = NativeModules;
|
||||
if (MattermostManaged) {
|
||||
this.appGroupIdentifier = MattermostManaged.appGroupIdentifier;
|
||||
}
|
||||
|
||||
if (this.appGroupIdentifier) {
|
||||
Emm.setAppGroupId(this.appGroupIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkIfDeviceIsTrusted = () => {
|
||||
const isTrusted = this.isTrustedDevice();
|
||||
|
||||
if (!isTrusted) {
|
||||
const locale = DEFAULT_LOCALE; // TODO: Get current user or system locale
|
||||
const translations = getTranslations(locale);
|
||||
Alert.alert(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
translations[t('mobile.managed.jailbreak')].replace('{vendor}', this.vendor),
|
||||
[{
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
Emm.exitApp();
|
||||
},
|
||||
}],
|
||||
{cancelable: false},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getAppGroupIdentifier = () => {
|
||||
return this.appGroupIdentifier;
|
||||
};
|
||||
|
||||
handleAuthentication = async (prompt = true) => {
|
||||
this.performingAuthentication = true;
|
||||
const isSecured = await Emm.isDeviceSecured();
|
||||
const locale = DEFAULT_LOCALE;
|
||||
const translations = getTranslations(locale);
|
||||
|
||||
if (isSecured) {
|
||||
try {
|
||||
if (prompt) {
|
||||
await Emm.authenticate({
|
||||
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor),
|
||||
fallback: true,
|
||||
supressEnterPassword: true,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Emm.exitApp();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
await this.showNotSecuredAlert(translations);
|
||||
|
||||
Emm.exitApp();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.performingAuthentication = false;
|
||||
return true;
|
||||
};
|
||||
|
||||
handleManagedConfig = async () => {
|
||||
if (this.performingAuthentication) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalConfig.AutoSelectServerUrl) {
|
||||
// TODO: Set server url based on EMM config
|
||||
this.allowOtherServers = false;
|
||||
}
|
||||
|
||||
const managedConfig = await Emm.getManagedConfig();
|
||||
|
||||
if (managedConfig && Object.keys(managedConfig).length) {
|
||||
this.enabled = true;
|
||||
this.inAppPinCode = managedConfig.inAppPinCode === 'true';
|
||||
|
||||
this.blurApplicationScreen = managedConfig.blurApplicationScreen === 'true';
|
||||
this.jailbreakProtection = managedConfig.jailbreakProtection === 'true';
|
||||
if (managedConfig.vendor) {
|
||||
this.vendor = managedConfig.vendor;
|
||||
}
|
||||
|
||||
const credentials = await getAppCredentials();
|
||||
if (!credentials) {
|
||||
if (managedConfig.serverUrl) {
|
||||
// TODO: Set server url based on EMM config
|
||||
}
|
||||
|
||||
if (managedConfig.allowOtherServers && managedConfig.allowOtherServers === 'false') {
|
||||
this.allowOtherServers = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.blurApplicationScreen) {
|
||||
Emm.enableBlurScreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
isTrustedDevice = () => {
|
||||
if (__DEV__) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return JailMonkey.trustFall();
|
||||
};
|
||||
|
||||
onAppStateChange = async (appState: AppStateStatus) => {
|
||||
const isActive = appState === 'active';
|
||||
const isBackground = appState === 'background';
|
||||
|
||||
if (isActive && (!this.enabled || this.previousAppState === 'background')) {
|
||||
// if the app is being controlled by an EMM provider
|
||||
if (this.enabled && this.inAppPinCode) {
|
||||
const authExpired = (Date.now() - this.backgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER;
|
||||
|
||||
// Once the app becomes active we check if the device needs to have a passcode set
|
||||
// if more than 5 minutes have passed prompt for passcode
|
||||
const prompt = this.backgroundSince > 0 && authExpired;
|
||||
await this.handleAuthentication(prompt);
|
||||
}
|
||||
|
||||
this.backgroundSince = 0;
|
||||
} else if (isBackground) {
|
||||
this.backgroundSince = Date.now();
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
};
|
||||
|
||||
showNotSecuredAlert = (translations: Record<string, string>) => {
|
||||
return new Promise(async (resolve) => { /* eslint-disable-line no-async-promise-executor */
|
||||
const buttons: AlertButton[] = [];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.settings')],
|
||||
onPress: () => {
|
||||
Emm.openSecuritySettings();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
onPress: resolve,
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
let message;
|
||||
if (Platform.OS === 'ios') {
|
||||
const {face} = await Emm.deviceSecureWith();
|
||||
|
||||
if (face) {
|
||||
message = translations[t('mobile.managed.not_secured.ios')];
|
||||
} else {
|
||||
message = translations[t('mobile.managed.not_secured.ios.touchId')];
|
||||
}
|
||||
} else {
|
||||
message = translations[t('mobile.managed.not_secured.android')];
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
message,
|
||||
buttons,
|
||||
{cancelable: false, onDismiss: () => resolve},
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default new EMMProvider();
|
||||
145
app/init/fetch.js
Normal file
145
app/init/fetch.js
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import urlParse from 'url-parse';
|
||||
|
||||
import LocalConfig from '@assets/config';
|
||||
import {Client4} from '@client/rest';
|
||||
import {HEADER_TOKEN, HEADER_X_CLUSTER_ID, HEADER_X_VERSION_ID} from '@client/rest/constants';
|
||||
import ClientError from '@client/rest/error';
|
||||
|
||||
// import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
// import {General} from '@mm-redux/constants';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
/* eslint-disable no-throw-literal */
|
||||
|
||||
const DEFAULT_TIMEOUT = 10000;
|
||||
|
||||
const handleRedirectProtocol = (url, response) => {
|
||||
const serverUrl = Client4.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) {
|
||||
Client4.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Client4.doFetchWithResponse = async (url, options) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Request endpoint', url);
|
||||
const customHeaders = LocalConfig.CustomRequestHeaders;
|
||||
|
||||
let requestOptions = {
|
||||
...Client4.getOptions(options),
|
||||
};
|
||||
|
||||
if (customHeaders && Object.keys(customHeaders).length > 0) {
|
||||
requestOptions = {
|
||||
...requestOptions,
|
||||
headers: {
|
||||
...requestOptions.headers,
|
||||
...LocalConfig.CustomRequestHeaders,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let response;
|
||||
let headers;
|
||||
|
||||
let data;
|
||||
try {
|
||||
response = await fetch(url, requestOptions);
|
||||
headers = response.headers;
|
||||
if (!url.startsWith('https') && response.rnfbRespInfo && response.rnfbRespInfo.redirects && response.rnfbRespInfo.redirects.length > 1) {
|
||||
handleRedirectProtocol(url, response);
|
||||
}
|
||||
|
||||
data = await response.json();
|
||||
} catch (err) {
|
||||
if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) {
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: 'You need to use a valid client certificate in order to connect to this Mattermost server',
|
||||
status_code: 401,
|
||||
url,
|
||||
details: err,
|
||||
});
|
||||
}
|
||||
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: 'Received invalid response from the server.',
|
||||
intl: {
|
||||
id: t('mobile.request.invalid_response'),
|
||||
defaultMessage: 'Received invalid response from the server.',
|
||||
},
|
||||
url,
|
||||
details: err,
|
||||
});
|
||||
}
|
||||
|
||||
const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
|
||||
if (clusterId && Client4.clusterId !== clusterId) {
|
||||
Client4.clusterId = clusterId; /* eslint-disable-line require-atomic-updates */
|
||||
}
|
||||
|
||||
const token = headers[HEADER_TOKEN] || headers[HEADER_TOKEN.toLowerCase()];
|
||||
if (token) {
|
||||
Client4.setToken(token);
|
||||
}
|
||||
|
||||
const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()];
|
||||
if (serverVersion && !headers['Cache-Control'] && Client4.serverVersion !== serverVersion) {
|
||||
Client4.serverVersion = serverVersion; /* eslint-disable-line require-atomic-updates */
|
||||
// EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const headersMap = new Map();
|
||||
Object.keys(headers).forEach((key) => {
|
||||
headersMap.set(key, headers[key]);
|
||||
});
|
||||
|
||||
return {
|
||||
response,
|
||||
headers: headersMap,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
const msg = data.message || '';
|
||||
|
||||
if (Client4.logToConsole) {
|
||||
console.error(msg); // eslint-disable-line no-console
|
||||
}
|
||||
|
||||
throw new ClientError(Client4.getUrl(), {
|
||||
message: msg,
|
||||
server_error_id: data.id,
|
||||
status_code: data.status_code,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
const initFetchConfig = async () => {
|
||||
const fetchConfig = {
|
||||
auto: true,
|
||||
timeout: DEFAULT_TIMEOUT, // Set the base timeout for every request to 5s
|
||||
};
|
||||
|
||||
const userAgent = await DeviceInfo.getUserAgent();
|
||||
Client4.setUserAgent(userAgent);
|
||||
|
||||
window.fetch = new RNFetchBlob.polyfill.Fetch(fetchConfig).build();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
initFetchConfig();
|
||||
|
||||
export default initFetchConfig;
|
||||
|
|
@ -10,14 +10,15 @@ import LocalConfig from '@assets/config.json';
|
|||
import {Navigation} from '@constants';
|
||||
import {DEFAULT_LOCALE, getTranslations, resetMomentLocale, t} from '@i18n';
|
||||
import * as analytics from '@init/analytics';
|
||||
import {getAppCredentials, removeAppCredentials} from '@init/credentials';
|
||||
import {getServerCredentials, removeServerCredentials} from '@init/credentials';
|
||||
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {LaunchType} from '@typings/launch';
|
||||
import {deleteFileCache} from '@utils/file';
|
||||
|
||||
type LinkingCallbackArg = {url: string};
|
||||
|
||||
class GlobalEventHandler {
|
||||
launchApp: launchAppFunc | undefined;
|
||||
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -29,9 +30,7 @@ class GlobalEventHandler {
|
|||
Linking.addEventListener('url', this.onDeepLink);
|
||||
}
|
||||
|
||||
configure = (opts: GlobalEventHandlerOpts) => {
|
||||
this.launchApp = opts.launchApp;
|
||||
|
||||
init = () => {
|
||||
this.JavascriptAndNativeErrorHandler = require('@utils/error_handling').default;
|
||||
this.JavascriptAndNativeErrorHandler?.initializeErrorHandling();
|
||||
};
|
||||
|
|
@ -48,11 +47,9 @@ class GlobalEventHandler {
|
|||
};
|
||||
|
||||
onDeepLink = (event: LinkingCallbackArg) => {
|
||||
const {url} = event;
|
||||
if (url) {
|
||||
// TODO: Handle deeplink
|
||||
// if server is not added go to add new server screen
|
||||
// if server is added but no credentials found take to server login screen
|
||||
if (event.url) {
|
||||
const props = getLaunchPropsFromDeepLink(event.url);
|
||||
relaunchApp(props);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -125,8 +122,9 @@ class GlobalEventHandler {
|
|||
analytics.invalidate(serverUrl);
|
||||
}
|
||||
|
||||
// TODO: remove credentials and files for the server
|
||||
removeAppCredentials();
|
||||
removeServerCredentials(serverUrl);
|
||||
|
||||
// TODO: remove files for the server
|
||||
deleteFileCache();
|
||||
PushNotifications.clearNotifications();
|
||||
|
||||
|
|
@ -136,11 +134,7 @@ class GlobalEventHandler {
|
|||
|
||||
await this.clearCookiesAndWebData();
|
||||
|
||||
if (this.launchApp) {
|
||||
this.launchApp(true);
|
||||
}
|
||||
|
||||
// TODO: Remove the database
|
||||
relaunchApp({launchType: LaunchType.Normal});
|
||||
};
|
||||
|
||||
onServerConfigChanged = (serverUrl: string, config: ClientConfig) => {
|
||||
|
|
@ -178,7 +172,7 @@ class GlobalEventHandler {
|
|||
};
|
||||
|
||||
serverUpgradeNeeded = async (serverUrl: string) => {
|
||||
const credentials = await getAppCredentials();
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
|
||||
if (credentials) {
|
||||
this.onLogout(serverUrl);
|
||||
|
|
|
|||
146
app/init/launch.ts
Normal file
146
app/init/launch.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Linking} from 'react-native';
|
||||
import {Notifications} from 'react-native-notifications';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {getActiveServerCredentials, getServerCredentials} from '@init/credentials';
|
||||
import {goToScreen, resetToChannel, resetToSelectServer} from '@screens/navigation';
|
||||
import {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkType, DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
|
||||
import {parseDeepLink} from '@utils/url';
|
||||
|
||||
export const initialLaunch = async () => {
|
||||
const deepLinkUrl = await Linking.getInitialURL();
|
||||
if (deepLinkUrl) {
|
||||
launchAppFromDeepLink(deepLinkUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await Notifications.getInitialNotification();
|
||||
if (notification) {
|
||||
launchAppFromNotification(notification);
|
||||
return;
|
||||
}
|
||||
|
||||
launchApp({launchType: LaunchType.Normal});
|
||||
};
|
||||
|
||||
const launchAppFromDeepLink = (deepLinkUrl: string) => {
|
||||
const props = getLaunchPropsFromDeepLink(deepLinkUrl);
|
||||
launchApp(props);
|
||||
};
|
||||
|
||||
const launchAppFromNotification = (notification: NotificationWithData) => {
|
||||
const props = getLaunchPropsFromNotification(notification);
|
||||
launchApp(props);
|
||||
};
|
||||
|
||||
const launchApp = async (props: LaunchProps, resetNavigation = true) => {
|
||||
let serverUrl;
|
||||
switch (props?.launchType) {
|
||||
case LaunchType.DeepLink:
|
||||
if (props.extra?.type !== DeepLinkType.Invalid) {
|
||||
const extra = props.extra as DeepLinkWithData;
|
||||
serverUrl = extra.data?.serverUrl;
|
||||
}
|
||||
break;
|
||||
case LaunchType.Notification: {
|
||||
const extra = props.extra as NotificationWithData;
|
||||
serverUrl = extra.payload?.server_url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = serverUrl ? await getServerCredentials(serverUrl) : await getActiveServerCredentials();
|
||||
|
||||
if (credentials) {
|
||||
launchToChannel(props, resetNavigation);
|
||||
return;
|
||||
}
|
||||
|
||||
launchToServer(props, resetNavigation);
|
||||
};
|
||||
|
||||
const launchToChannel = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
// TODO: Use LaunchProps to fetch posts for channel and then load user profile, etc...
|
||||
|
||||
const passProps = {
|
||||
skipMetrics: true,
|
||||
...props,
|
||||
};
|
||||
|
||||
if (resetNavigation) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Channel screen');
|
||||
resetToChannel(passProps);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = '';
|
||||
goToScreen(Screens.CHANNEL, title, passProps);
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
if (resetNavigation) {
|
||||
resetToSelectServer(props);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = '';
|
||||
goToScreen(Screens.SERVER, title, {...props});
|
||||
};
|
||||
|
||||
export const relaunchApp = (props: LaunchProps) => {
|
||||
launchApp(props, false);
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps => {
|
||||
const parsed = parseDeepLink(deepLinkUrl);
|
||||
const launchProps: LaunchProps = {
|
||||
launchType: LaunchType.DeepLink,
|
||||
};
|
||||
|
||||
switch (parsed.type) {
|
||||
case DeepLinkType.Invalid:
|
||||
launchProps.launchError = true;
|
||||
break;
|
||||
case DeepLinkType.Channel: {
|
||||
const parsedData = parsed.data as DeepLinkChannel;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.DirectMessage: {
|
||||
const parsedData = parsed.data as DeepLinkDM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.GroupMessage: {
|
||||
const parsedData = parsed.data as DeepLinkGM;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
case DeepLinkType.Permalink: {
|
||||
const parsedData = parsed.data as DeepLinkPermalink;
|
||||
(launchProps.extra as DeepLinkWithData).data = parsedData;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return launchProps;
|
||||
};
|
||||
|
||||
export const getLaunchPropsFromNotification = (notification: NotificationWithData): LaunchProps => {
|
||||
const {payload} = notification;
|
||||
const launchProps: LaunchProps = {
|
||||
launchType: LaunchType.Notification,
|
||||
};
|
||||
|
||||
if (payload?.server_url) {
|
||||
(launchProps.extra as NotificationWithData) = notification;
|
||||
} else {
|
||||
launchProps.launchError = true;
|
||||
}
|
||||
|
||||
return launchProps;
|
||||
};
|
||||
190
app/init/managed_app.ts
Normal file
190
app/init/managed_app.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Emm, {ManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {Alert, AlertButton, AppState, AppStateStatus, Platform} from 'react-native';
|
||||
import JailMonkey from 'jail-monkey';
|
||||
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
const PROMPT_IN_APP_PIN_CODE_AFTER = 5 * 1000;
|
||||
|
||||
class ManagedApp {
|
||||
backgroundSince = 0;
|
||||
enabled = false;
|
||||
inAppPinCode = false;
|
||||
performingAuthentication = false;
|
||||
previousAppState?: AppStateStatus;
|
||||
processConfigTimeout?: NodeJS.Timeout;
|
||||
vendor = 'Mattermost';
|
||||
|
||||
constructor() {
|
||||
Emm.addListener(this.processConfig);
|
||||
|
||||
this.setIOSAppGroupIdentifier();
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
}
|
||||
|
||||
init() {
|
||||
Emm.getManagedConfig().then(this.processConfig);
|
||||
}
|
||||
|
||||
setIOSAppGroupIdentifier = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {appGroupIdentifier} = getIOSAppGroupDetails();
|
||||
|
||||
if (appGroupIdentifier) {
|
||||
Emm.setAppGroupId(appGroupIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processConfig = async (config?: ManagedConfig) => {
|
||||
// If the managed configuration changed while authentication was
|
||||
// being performed, delay the processing of this new configuration
|
||||
// until authentication is complete.
|
||||
if (this.performingAuthentication) {
|
||||
if (this.processConfigTimeout) {
|
||||
clearTimeout(this.processConfigTimeout);
|
||||
}
|
||||
|
||||
this.processConfigTimeout = setTimeout(() => this.processConfig(config), 500);
|
||||
}
|
||||
|
||||
this.enabled = Boolean(config && Object.keys(config).length);
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blurScreen = config!.blurApplicationScreen === 'true';
|
||||
Emm.enableBlurScreen(blurScreen);
|
||||
|
||||
const vendor = config!.vendor;
|
||||
if (vendor) {
|
||||
this.vendor = vendor;
|
||||
}
|
||||
|
||||
const jailbreakProtection = config!.jailbreakProtection === 'true';
|
||||
if (jailbreakProtection && !this.isTrustedDevice()) {
|
||||
this.alertDeviceIsUntrusted();
|
||||
return;
|
||||
}
|
||||
|
||||
const inAppPinCode = config!.inAppPinCode === 'true';
|
||||
if (inAppPinCode) {
|
||||
this.handleDeviceAuthentication();
|
||||
}
|
||||
};
|
||||
|
||||
alertDeviceIsUntrusted = () => {
|
||||
const locale = DEFAULT_LOCALE; // TODO: Get current user or system locale
|
||||
const translations = getTranslations(locale);
|
||||
Alert.alert(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
translations[t('mobile.managed.jailbreak')].replace('{vendor}', this.vendor),
|
||||
[{
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
Emm.exitApp();
|
||||
},
|
||||
}],
|
||||
{cancelable: false},
|
||||
);
|
||||
};
|
||||
|
||||
handleDeviceAuthentication = async (authExpired = true) => {
|
||||
this.performingAuthentication = true;
|
||||
const isSecured = await Emm.isDeviceSecured();
|
||||
const locale = DEFAULT_LOCALE;
|
||||
const translations = getTranslations(locale);
|
||||
|
||||
if (!isSecured) {
|
||||
await this.showNotSecuredAlert(translations);
|
||||
Emm.exitApp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (authExpired) {
|
||||
try {
|
||||
await Emm.authenticate({
|
||||
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor),
|
||||
fallback: true,
|
||||
supressEnterPassword: true,
|
||||
});
|
||||
} catch (err) {
|
||||
Emm.exitApp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.performingAuthentication = false;
|
||||
};
|
||||
|
||||
isTrustedDevice = () => {
|
||||
return __DEV__ || JailMonkey.trustFall();
|
||||
};
|
||||
|
||||
onAppStateChange = async (appState: AppStateStatus) => {
|
||||
const isActive = appState === 'active';
|
||||
const isBackground = appState === 'background';
|
||||
|
||||
if (isActive && this.previousAppState === 'background') {
|
||||
if (this.enabled && this.inAppPinCode) {
|
||||
const authExpired = this.backgroundSince > 0 && (Date.now() - this.backgroundSince) >= PROMPT_IN_APP_PIN_CODE_AFTER;
|
||||
await this.handleDeviceAuthentication(authExpired);
|
||||
}
|
||||
|
||||
this.backgroundSince = 0;
|
||||
} else if (isBackground) {
|
||||
this.backgroundSince = Date.now();
|
||||
}
|
||||
|
||||
this.previousAppState = appState;
|
||||
};
|
||||
|
||||
showNotSecuredAlert = (translations: Record<string, string>) => {
|
||||
return new Promise(async (resolve) => { /* eslint-disable-line no-async-promise-executor */
|
||||
const buttons: AlertButton[] = [];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.settings')],
|
||||
onPress: () => {
|
||||
Emm.openSecuritySettings();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
onPress: resolve,
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
let message;
|
||||
if (Platform.OS === 'ios') {
|
||||
const {face} = await Emm.deviceSecureWith();
|
||||
|
||||
if (face) {
|
||||
message = translations[t('mobile.managed.not_secured.ios')];
|
||||
} else {
|
||||
message = translations[t('mobile.managed.not_secured.ios.touchId')];
|
||||
}
|
||||
} else {
|
||||
message = translations[t('mobile.managed.not_secured.android')];
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
message,
|
||||
buttons,
|
||||
{cancelable: false, onDismiss: () => resolve},
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default new ManagedApp();
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import {getActiveServerDatabase} from '@utils/database';
|
||||
import {AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Platform} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {
|
||||
|
|
@ -14,11 +16,12 @@ import {
|
|||
Registered,
|
||||
} from 'react-native-notifications';
|
||||
|
||||
import {dismissAllModals, popToRoot, showOverlay} from '@screens/navigation';
|
||||
import {Device, Navigation, View} from '@constants';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Operator from '@database/operator';
|
||||
import {getLaunchPropsFromNotification, relaunchApp} from '@init/launch';
|
||||
import NativeNotifications from '@notifications';
|
||||
import {showOverlay} from '@screens/navigation';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
|
||||
const CATEGORY = 'CAN_REPLY';
|
||||
const REPLY_ACTION = 'REPLY_ACTION';
|
||||
|
|
@ -28,217 +31,217 @@ const NOTIFICATION_TYPE = {
|
|||
SESSION: 'session',
|
||||
};
|
||||
|
||||
//todo: Do we need Ephemeral store? Should we refactor this file ?
|
||||
|
||||
class PushNotifications {
|
||||
configured = false;
|
||||
pushNotificationListener: EmitterSubscription | undefined;
|
||||
configured = false;
|
||||
pushNotificationListener: EmitterSubscription | undefined;
|
||||
|
||||
constructor() {
|
||||
Notifications.registerRemoteNotifications();
|
||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
constructor() {
|
||||
Notifications.registerRemoteNotifications();
|
||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
}
|
||||
|
||||
this.getInitialNotification();
|
||||
}
|
||||
cancelAllLocalNotifications = () => {
|
||||
Notifications.cancelAllLocalNotifications();
|
||||
};
|
||||
|
||||
cancelAllLocalNotifications = () => {
|
||||
Notifications.cancelAllLocalNotifications();
|
||||
};
|
||||
clearNotifications = () => {
|
||||
this.setApplicationIconBadgeNumber(0);
|
||||
|
||||
clearNotifications = () => {
|
||||
this.setApplicationIconBadgeNumber(0);
|
||||
// TODO: Only cancel the local notifications that belong to this server
|
||||
this.cancelAllLocalNotifications();
|
||||
};
|
||||
|
||||
// TODO: Only cancel the local notifications that belong to this server
|
||||
this.cancelAllLocalNotifications();
|
||||
};
|
||||
clearChannelNotifications = async (channelId: string) => {
|
||||
const notifications = await NativeNotifications.getDeliveredNotifications();
|
||||
if (Platform.OS === 'android') {
|
||||
const notificationForChannel = notifications.find(
|
||||
(n: NotificationWithChannel) => n.channel_id === channelId,
|
||||
);
|
||||
if (notificationForChannel) {
|
||||
NativeNotifications.removeDeliveredNotifications(
|
||||
notificationForChannel.identifier,
|
||||
channelId,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const ids: string[] = [];
|
||||
const badgeCount = notifications.length;
|
||||
|
||||
clearChannelNotifications = async (channelId: string) => {
|
||||
const notifications = await NativeNotifications.getDeliveredNotifications();
|
||||
if (Platform.OS === 'android') {
|
||||
const notificationForChannel = notifications.find((n: NotificationWithChannel) => n.channel_id === channelId);
|
||||
if (notificationForChannel) {
|
||||
NativeNotifications.removeDeliveredNotifications(notificationForChannel.identifier, channelId);
|
||||
}
|
||||
} else {
|
||||
const ids: string[] = [];
|
||||
const badgeCount = notifications.length;
|
||||
// TODO: Set the badgeCount with default database mention count aggregate
|
||||
|
||||
// TODO: Set the badgeCount with default database mention count aggregate
|
||||
for (const notification of notifications) {
|
||||
if (notification.channel_id === channelId) {
|
||||
ids.push(notification.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
if (notification.channel_id === channelId) {
|
||||
ids.push(notification.identifier);
|
||||
}
|
||||
}
|
||||
if (ids.length) {
|
||||
NativeNotifications.removeDeliveredNotifications(ids);
|
||||
}
|
||||
|
||||
if (ids.length) {
|
||||
NativeNotifications.removeDeliveredNotifications(ids);
|
||||
}
|
||||
this.setApplicationIconBadgeNumber(badgeCount);
|
||||
}
|
||||
};
|
||||
|
||||
this.setApplicationIconBadgeNumber(badgeCount);
|
||||
}
|
||||
};
|
||||
createReplyCategory = () => {
|
||||
const locale = DEFAULT_LOCALE; // TODO: Get the current user locale to replace the old getCurrentLocale(state);
|
||||
|
||||
createReplyCategory = () => {
|
||||
const locale = DEFAULT_LOCALE; // TODO: Get the current user locale to replace the old getCurrentLocale(state);
|
||||
const replyTitle = getLocalizedMessage(locale, t('mobile.push_notification_reply.title'));
|
||||
const replyButton = getLocalizedMessage(locale, t('mobile.push_notification_reply.button'));
|
||||
const replyPlaceholder = getLocalizedMessage(locale, t('mobile.push_notification_reply.placeholder'));
|
||||
const replyTextInput: NotificationTextInput = {buttonTitle: replyButton, placeholder: replyPlaceholder};
|
||||
const replyAction = new NotificationAction(REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
|
||||
return new NotificationCategory(CATEGORY, [replyAction]);
|
||||
};
|
||||
|
||||
const replyTitle = getLocalizedMessage(locale, t('mobile.push_notification_reply.title'));
|
||||
const replyButton = getLocalizedMessage(locale, t('mobile.push_notification_reply.button'));
|
||||
const replyPlaceholder = getLocalizedMessage(locale, t('mobile.push_notification_reply.placeholder'));
|
||||
const replyTextInput: NotificationTextInput = {
|
||||
buttonTitle: replyButton,
|
||||
placeholder: replyPlaceholder,
|
||||
};
|
||||
const replyAction = new NotificationAction(REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
|
||||
return new NotificationCategory(CATEGORY, [replyAction]);
|
||||
};
|
||||
handleNotification = async (notification: NotificationWithData) => {
|
||||
const {payload, foreground, userInteraction} = notification;
|
||||
|
||||
getInitialNotification = async () => {
|
||||
const notification: NotificationWithData | undefined = await Notifications.getInitialNotification();
|
||||
if (payload) {
|
||||
switch (payload.type) {
|
||||
case NOTIFICATION_TYPE.CLEAR:
|
||||
// Mark the channel as read
|
||||
break;
|
||||
case NOTIFICATION_TYPE.MESSAGE:
|
||||
// fetch the posts for the channel
|
||||
|
||||
if (notification) {
|
||||
EphemeralStore.setStartFromNotification(true);
|
||||
notification.userInteraction = true;
|
||||
this.handleNotification(notification);
|
||||
}
|
||||
};
|
||||
if (foreground) {
|
||||
// Show the in-app notification
|
||||
} else if (userInteraction && !payload.userInfo?.local) {
|
||||
const props = getLaunchPropsFromNotification(notification);
|
||||
relaunchApp(props);
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPE.SESSION:
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Session expired notification');
|
||||
|
||||
handleNotification = async (notification: NotificationWithData) => {
|
||||
const {payload, foreground, userInteraction} = notification;
|
||||
// Logout the user from the server that matches the notification
|
||||
|
||||
if (payload) {
|
||||
switch (payload.type) {
|
||||
case NOTIFICATION_TYPE.CLEAR:
|
||||
// Mark the channel as read
|
||||
break;
|
||||
case NOTIFICATION_TYPE.MESSAGE:
|
||||
// fetch the posts for the channel
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (foreground) {
|
||||
// Show the in-app notification
|
||||
} else if (userInteraction && !payload.userInfo?.local) {
|
||||
// Swith to the server / team / channel that matches the notification
|
||||
handleInAppNotification = async (notification: NotificationWithData) => {
|
||||
const {payload} = notification;
|
||||
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
if (componentId) {
|
||||
// Emit events to close the sidebars
|
||||
// TODO: Get current channel from the database
|
||||
const currentChannelId = '';
|
||||
|
||||
await dismissAllModals();
|
||||
await popToRoot();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPE.SESSION:
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Session expired notification');
|
||||
if (payload?.channel_id !== currentChannelId) {
|
||||
const screen = 'Notification';
|
||||
const passProps = {
|
||||
notification,
|
||||
};
|
||||
|
||||
// Logout the user from the server that matches the notification
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
|
||||
showOverlay(screen, passProps);
|
||||
}
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
localNotification = (notification: Notification) => {
|
||||
Notifications.postLocalNotification(notification);
|
||||
};
|
||||
|
||||
handleInAppNotification = async (notification: NotificationWithData) => {
|
||||
const {payload} = notification;
|
||||
onAppStateChange = (appState: AppStateStatus) => {
|
||||
const isActive = appState === 'active';
|
||||
const isBackground = appState === 'background';
|
||||
|
||||
// TODO: Get current channel from the database
|
||||
const currentChannelId = '';
|
||||
if (isActive) {
|
||||
if (!this.pushNotificationListener) {
|
||||
this.pushNotificationListener = DeviceEventEmitter.addListener(
|
||||
View.NOTIFICATION_IN_APP,
|
||||
this.handleInAppNotification,
|
||||
);
|
||||
}
|
||||
} else if (isBackground) {
|
||||
this.pushNotificationListener?.remove();
|
||||
this.pushNotificationListener = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (payload?.channel_id !== currentChannelId) {
|
||||
const screen = 'Notification';
|
||||
const passProps = {
|
||||
notification,
|
||||
};
|
||||
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
|
||||
notification.userInteraction = true;
|
||||
this.handleNotification(notification);
|
||||
completion();
|
||||
};
|
||||
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
|
||||
showOverlay(screen, passProps);
|
||||
}
|
||||
}
|
||||
onNotificationReceivedBackground = (notification: NotificationWithData, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
||||
this.handleNotification(notification);
|
||||
completion(NotificationBackgroundFetchResult.NO_DATA);
|
||||
};
|
||||
|
||||
localNotification = (notification: Notification) => {
|
||||
Notifications.postLocalNotification(notification);
|
||||
};
|
||||
onNotificationReceivedForeground = (notification: NotificationWithData, completion: (response: NotificationCompletion) => void) => {
|
||||
notification.foreground = AppState.currentState === 'active';
|
||||
completion({alert: false, sound: true, badge: true});
|
||||
this.handleNotification(notification);
|
||||
};
|
||||
|
||||
onAppStateChange = (appState: AppStateStatus) => {
|
||||
const isActive = appState === 'active';
|
||||
const isBackground = appState === 'background';
|
||||
onRemoteNotificationsRegistered = async (event: Registered) => {
|
||||
if (!this.configured) {
|
||||
this.configured = true;
|
||||
const {deviceToken} = event;
|
||||
let prefix;
|
||||
|
||||
if (isActive) {
|
||||
if (!this.pushNotificationListener) {
|
||||
this.pushNotificationListener = DeviceEventEmitter.addListener(View.NOTIFICATION_IN_APP, this.handleInAppNotification);
|
||||
}
|
||||
} else if (isBackground) {
|
||||
this.pushNotificationListener?.remove();
|
||||
this.pushNotificationListener = undefined;
|
||||
}
|
||||
};
|
||||
if (Platform.OS === 'ios') {
|
||||
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
||||
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||
prefix = `${prefix}beta`;
|
||||
}
|
||||
} else {
|
||||
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
||||
}
|
||||
|
||||
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
|
||||
notification.userInteraction = true;
|
||||
this.handleNotification(notification);
|
||||
completion();
|
||||
};
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
|
||||
onNotificationReceivedBackground = (notification: NotificationWithData, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
||||
this.handleNotification(notification);
|
||||
completion(NotificationBackgroundFetchResult.NO_DATA);
|
||||
};
|
||||
if (!activeServerDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
onNotificationReceivedForeground = (notification: NotificationWithData, completion: (response: NotificationCompletion) => void) => {
|
||||
notification.foreground = AppState.currentState === 'active';
|
||||
completion({alert: false, sound: true, badge: true});
|
||||
this.handleNotification(notification);
|
||||
};
|
||||
const operator = new Operator(activeServerDatabase);
|
||||
|
||||
onRemoteNotificationsRegistered = (event: Registered) => {
|
||||
if (!this.configured) {
|
||||
this.configured = true;
|
||||
const {deviceToken} = event;
|
||||
let prefix;
|
||||
operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.GLOBAL,
|
||||
values: [{name: 'deviceToken', value: `${prefix}:${deviceToken}`}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
||||
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||
prefix = `${prefix}beta`;
|
||||
}
|
||||
} else {
|
||||
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
||||
}
|
||||
// Store the device token in the default database
|
||||
this.requestNotificationReplyPermissions();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
EphemeralStore.deviceToken = `${prefix}:${deviceToken}`;
|
||||
requestNotificationReplyPermissions = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const replyCategory = this.createReplyCategory();
|
||||
Notifications.setCategories([replyCategory]);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the device token in the default database
|
||||
this.requestNotificationReplyPermissions();
|
||||
}
|
||||
};
|
||||
setApplicationIconBadgeNumber = (value: number) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const count = value < 0 ? 0 : value;
|
||||
Notifications.ios.setBadgeCount(count);
|
||||
}
|
||||
};
|
||||
|
||||
requestNotificationReplyPermissions = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const replyCategory = this.createReplyCategory();
|
||||
Notifications.setCategories([replyCategory]);
|
||||
}
|
||||
};
|
||||
scheduleNotification = (notification: Notification) => {
|
||||
if (notification.fireDate) {
|
||||
if (Platform.OS === 'ios') {
|
||||
notification.fireDate = new Date(notification.fireDate).toISOString();
|
||||
}
|
||||
|
||||
setApplicationIconBadgeNumber = (value: number) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const count = value < 0 ? 0 : value;
|
||||
Notifications.ios.setBadgeCount(count);
|
||||
}
|
||||
};
|
||||
|
||||
scheduleNotification = (notification: Notification) => {
|
||||
if (notification.fireDate) {
|
||||
if (Platform.OS === 'ios') {
|
||||
notification.fireDate = new Date(notification.fireDate).toISOString();
|
||||
}
|
||||
|
||||
Notifications.postLocalNotification(notification);
|
||||
}
|
||||
};
|
||||
Notifications.postLocalNotification(notification);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new PushNotifications();
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter, Linking} from 'react-native';
|
||||
import {ComponentDidAppearEvent, ComponentDidDisappearEvent, Navigation} from 'react-native-navigation';
|
||||
|
||||
import {Navigation as NavigationConstants, Screens} from '@constants';
|
||||
import {getAppCredentials} from '@init/credentials';
|
||||
import emmProvider from '@init/emm_provider';
|
||||
import globalEventHandler from '@init/global_event_handler';
|
||||
import {registerScreens} from '@screens/index';
|
||||
import {resetToChannel, resetToSelectServer} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
const init = () => {
|
||||
if (EphemeralStore.appStarted) {
|
||||
launchApp(true);
|
||||
return;
|
||||
}
|
||||
|
||||
globalEventHandler.configure({
|
||||
launchApp,
|
||||
});
|
||||
|
||||
registerScreens();
|
||||
|
||||
if (!EphemeralStore.appStarted) {
|
||||
launchApp(false);
|
||||
}
|
||||
};
|
||||
|
||||
const launchApp = async (skipEmm: boolean) => {
|
||||
if (!skipEmm) {
|
||||
await emmProvider.handleManagedConfig();
|
||||
if (emmProvider.enabled) {
|
||||
if (emmProvider.jailbreakProtection) {
|
||||
emmProvider.checkIfDeviceIsTrusted();
|
||||
}
|
||||
|
||||
if (emmProvider.inAppPinCode) {
|
||||
await emmProvider.handleAuthentication();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
// TODO: Handle deeplink
|
||||
// if server is not added go to add new server screen
|
||||
// if server is added but no credentials found take to server login screen
|
||||
}
|
||||
});
|
||||
|
||||
const credentials = await getAppCredentials();
|
||||
if (credentials) {
|
||||
// TODO: Fetch posts for current channel and then load user profile, etc..
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Channel screen');
|
||||
resetToChannel({skipMetrics: true});
|
||||
} else {
|
||||
resetToSelectServer(emmProvider.allowOtherServers);
|
||||
}
|
||||
|
||||
EphemeralStore.appStarted = true;
|
||||
};
|
||||
|
||||
Navigation.events().registerAppLaunchedListener(() => {
|
||||
init();
|
||||
|
||||
// Keep track of the latest componentId to appear/disappear
|
||||
Navigation.events().registerComponentDidAppearListener(componentDidAppearListener);
|
||||
Navigation.events().registerComponentDidDisappearListener(componentDidDisappearListener);
|
||||
});
|
||||
|
||||
export function componentDidAppearListener({componentId}: ComponentDidAppearEvent) {
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
|
||||
switch (componentId) {
|
||||
case 'MainSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_OPEN, this.handleSidebarDidOpen);
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
case 'SettingsSidebar':
|
||||
DeviceEventEmitter.emit(NavigationConstants.BLUR_POST_DRAFT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function componentDidDisappearListener({componentId}: ComponentDidDisappearEvent) {
|
||||
if (componentId !== Screens.CHANNEL) {
|
||||
EphemeralStore.removeNavigationComponentId(componentId);
|
||||
}
|
||||
|
||||
if (componentId === 'MainSidebar') {
|
||||
DeviceEventEmitter.emit(NavigationConstants.MAIN_SIDEBAR_DID_CLOSE);
|
||||
}
|
||||
}
|
||||
13
app/queries/global.ts
Normal file
13
app/queries/global.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
import Global from '@typings/database/global';
|
||||
|
||||
const {DEFAULT: {GLOBAL}} = MM_TABLES;
|
||||
|
||||
export const getDeviceToken = async (defaultDatabase: Database) => {
|
||||
const tokens = (await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', 'deviceToken')).fetch()) as Global[];
|
||||
return tokens?.[0]?.value ?? '';
|
||||
};
|
||||
14
app/queries/role.ts
Normal file
14
app/queries/role.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import Role from '@typings/database/role';
|
||||
|
||||
const {SERVER: {ROLE}} = MM_TABLES;
|
||||
|
||||
export const getRoles = async (database: Database) => {
|
||||
const roles = await database.collections.get(ROLE).query().fetch() as Role[];
|
||||
return roles;
|
||||
};
|
||||
38
app/queries/system.ts
Normal file
38
app/queries/system.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import System from '@typings/database/system';
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
|
||||
export const getCurrentUserId = async (serverDatabase: Database) => {
|
||||
const currentUserId = await serverDatabase.collections.get(SYSTEM).query(Q.where('name', 'currentUserId')).fetch() as System[];
|
||||
return currentUserId?.[0] ?? '';
|
||||
};
|
||||
|
||||
export const getCommonSystemValues = async (database: Database) => {
|
||||
const systemRecords = (await database.collections.get(SYSTEM).query(Q.where('name', Q.oneOf(['config', 'license', 'currentUserId']))).fetch()) as System[];
|
||||
let config = {};
|
||||
let license = {};
|
||||
let currentUserId = '';
|
||||
systemRecords.forEach((systemRecord) => {
|
||||
if (systemRecord.name === 'config') {
|
||||
config = systemRecord.value;
|
||||
}
|
||||
if (systemRecord.name === 'license') {
|
||||
license = systemRecord.value;
|
||||
}
|
||||
if (systemRecord.name === 'currentUserId') {
|
||||
currentUserId = systemRecord.value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
config,
|
||||
license,
|
||||
};
|
||||
};
|
||||
12
app/queries/user.ts
Normal file
12
app/queries/user.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import User from '@typings/database/user';
|
||||
|
||||
export const getUserById = async ({userId, database}: { userId: string, database: Database}) => {
|
||||
const userRecords = (await database.collections.get(MM_TABLES.SERVER.USER).query(Q.where('id', userId)).fetch()) as User[];
|
||||
return userRecords?.[0];
|
||||
};
|
||||
84
app/requests/local/systems.ts
Normal file
84
app/requests/local/systems.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import Operator from '@database/operator';
|
||||
import {ServerUrlChangedArgs} from '@typings/database/database';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
import System from '@typings/database/system';
|
||||
import {getActiveServerDatabase} from '@utils/database';
|
||||
|
||||
/**
|
||||
* setLastUpgradeCheck: Takes in 'config' record from System entity and update its lastUpdateCheck to Date.now()
|
||||
* @param {System} configRecord
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const setLastUpgradeCheck = async (configRecord: System) => {
|
||||
const {activeServerDatabase: database, error} = await getActiveServerDatabase();
|
||||
if (!database) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
await database.action(async () => {
|
||||
await configRecord.update((config) => {
|
||||
config.value = {...configRecord.value, lastUpdateCheck: Date.now()};
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handleServerUrlChanged = async ({configRecord, licenseRecord, selectServerRecord, serverUrl}: ServerUrlChangedArgs) => {
|
||||
const {activeServerDatabase: database, error} = await getActiveServerDatabase();
|
||||
if (!database) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
await database.action(async () => {
|
||||
await database.batch(
|
||||
...[
|
||||
configRecord.prepareUpdate((config: System) => {
|
||||
config.value = {};
|
||||
}),
|
||||
licenseRecord.prepareUpdate((license: System) => {
|
||||
license.value = {};
|
||||
}),
|
||||
selectServerRecord.prepareUpdate((server: System) => {
|
||||
server.value = {...server.value, serverUrl};
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createSessions = async (sessions: any) => {
|
||||
const {activeServerDatabase: database, error} = await getActiveServerDatabase();
|
||||
if (!database) {
|
||||
return {error};
|
||||
}
|
||||
const operator = new Operator(database);
|
||||
await operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values: [{
|
||||
|
||||
// id: string; // todo: to confirm value for session id ?
|
||||
name: 'sessions',
|
||||
value: sessions,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
export const setDeepLinkUrl = async (url: string) => {
|
||||
const operator = new Operator();
|
||||
await operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.GLOBAL,
|
||||
values: [{
|
||||
name: 'deepLinkUrl',
|
||||
value: url,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
};
|
||||
56
app/requests/local/timezone.ts
Normal file
56
app/requests/local/timezone.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getTimeZone} from 'react-native-localize';
|
||||
|
||||
import {getUserById} from '@queries/user';
|
||||
import {updateMe} from '@requests/remote/user';
|
||||
import {Config} from '@typings/database/config';
|
||||
import User from '@typings/database/user';
|
||||
import {getActiveServerDatabase} from '@utils/database';
|
||||
|
||||
export const isTimezoneEnabled = (config: Partial<Config>) => {
|
||||
return config?.ExperimentalTimezone === 'true';
|
||||
};
|
||||
|
||||
export function getDeviceTimezone() {
|
||||
return getTimeZone();
|
||||
}
|
||||
|
||||
export const autoUpdateTimezone = async ({deviceTimezone, userId}: {deviceTimezone: string, userId: string}) => {
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
if (!activeServerDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const currentUser = await getUserById({userId, database: activeServerDatabase}) ?? null;
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTimezone = getUserTimezone(currentUser);
|
||||
const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone;
|
||||
|
||||
if (currentTimezone.useAutomaticTimezone && newTimezoneExists) {
|
||||
const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone};
|
||||
const updatedUser = {...currentUser, timezone} as User;
|
||||
await updateMe(updatedUser);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getUserTimezone = (currentUser: User) => {
|
||||
if (currentUser?.timezone) {
|
||||
return {
|
||||
...currentUser?.timezone,
|
||||
useAutomaticTimezone: currentUser?.timezone?.useAutomaticTimezone === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
useAutomaticTimezone: true,
|
||||
automaticTimezone: '',
|
||||
manualTimezone: '',
|
||||
};
|
||||
};
|
||||
39
app/requests/remote/error.ts
Normal file
39
app/requests/remote/error.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//fixme: to be completed in next PR.
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const logError = (_e: any) => null;
|
||||
|
||||
// export const logError = async (error: Error, displayable = false) => {
|
||||
// if (error?.server_error_id === 'api.context.session_expired.app_error') {
|
||||
// return {data: true};
|
||||
// }
|
||||
//
|
||||
// const serializedError = serializeError(error);
|
||||
//
|
||||
// let sendToServer = true;
|
||||
// if (error.stack && error.stack.includes('TypeError: Failed to fetch')) {
|
||||
// sendToServer = false;
|
||||
// }
|
||||
// if (error?.server_error_id) {
|
||||
// sendToServer = false;
|
||||
// }
|
||||
//
|
||||
// if (sendToServer) {
|
||||
// try {
|
||||
// const stringifiedSerializedError = JSON.stringify(serializedError).toString();
|
||||
// await Client4.logClientError(stringifiedSerializedError);
|
||||
// } catch (err) {
|
||||
// // avoid crashing the app if an error sending
|
||||
// // the error occurs.
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// EventEmitter.emit(ErrorTypes.LOG_ERROR, error);
|
||||
// dispatch(getLogErrorAction(serializedError, displayable));
|
||||
|
||||
// return {data: true};
|
||||
// };
|
||||
|
||||
46
app/requests/remote/general.ts
Normal file
46
app/requests/remote/general.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
|
||||
export const doPing = async (serverUrl?: string) => {
|
||||
let data;
|
||||
const pingError = {
|
||||
id: 'mobile.server_ping_failed',
|
||||
defaultMessage: 'Cannot connect to the server. Please check your server URL and internet connection.',
|
||||
};
|
||||
|
||||
try {
|
||||
if (serverUrl) {
|
||||
Client4.setUrl(serverUrl);
|
||||
}
|
||||
|
||||
data = await Client4.ping();
|
||||
if (data.status !== 'OK') {
|
||||
// successful ping but not the right return {data}
|
||||
return {error: {intl: pingError}};
|
||||
}
|
||||
} catch (error) {
|
||||
// Client4Error
|
||||
if (error.status_code === 401) {
|
||||
// When the server requires a client certificate to connect.
|
||||
return {error};
|
||||
}
|
||||
return {error: {intl: pingError}};
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
|
||||
export const fetchConfigAndLicense = async () => {
|
||||
try {
|
||||
const [config, license] = await Promise.all<ClientConfig, ClientLicense>([
|
||||
Client4.getClientConfigOld(),
|
||||
Client4.getClientLicenseOld(),
|
||||
]);
|
||||
|
||||
return {config, license};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
82
app/requests/remote/push_notification.ts
Normal file
82
app/requests/remote/push_notification.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {getCommonSystemValues} from '@queries/system';
|
||||
import {getSessions} from '@requests/remote/user';
|
||||
import {Config} from '@typings/database/config';
|
||||
import {getActiveServerDatabase} from '@utils/database';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
const MAJOR_VERSION = 5;
|
||||
const MINOR_VERSION = 24;
|
||||
|
||||
const sortByNewest = (a: any, b: any) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
export const scheduleExpiredNotification = async (intl: IntlShape) => {
|
||||
const {activeServerDatabase: database, error} = await getActiveServerDatabase();
|
||||
if (!database) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const {currentUserId, config}: {currentUserId: string, config: Partial<Config>} = await getCommonSystemValues(database);
|
||||
|
||||
if (isMinimumServerVersion(Client4.serverVersion, MAJOR_VERSION, MINOR_VERSION) && config.ExtendSessionLengthWithActivity === 'true') {
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeOut = setTimeout(async () => {
|
||||
clearTimeout(timeOut);
|
||||
let sessions: any;
|
||||
|
||||
try {
|
||||
sessions = await getSessions(currentUserId);
|
||||
} catch (e) {
|
||||
// console.warn('Failed to get current session', e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(sessions?.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const session = sessions.data.sort(sortByNewest)[0];
|
||||
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
|
||||
const expiresInDays = parseInt(String(Math.ceil(Math.abs(moment.duration(moment().diff(expiresAt)).asDays()))), 10);
|
||||
|
||||
const message = intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.'},
|
||||
{
|
||||
siteName: config.SiteName,
|
||||
daysCount: expiresInDays,
|
||||
},
|
||||
);
|
||||
|
||||
if (expiresAt) {
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
userInfo: {
|
||||
local: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
return null;
|
||||
};
|
||||
39
app/requests/remote/role.ts
Normal file
39
app/requests/remote/role.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import Operator from '@database/operator';
|
||||
import {getRoles} from '@queries/role';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
import {getActiveServerDatabase} from '@utils/database';
|
||||
|
||||
export const loadRolesIfNeeded = async (updatedRoles: string[]) => {
|
||||
const {activeServerDatabase: database, error: e} = await getActiveServerDatabase();
|
||||
if (!database) {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
const existingRoles = ((await getRoles(database)) as unknown) as Role[];
|
||||
|
||||
const roleNames = existingRoles.map((role) => {
|
||||
return role.name;
|
||||
});
|
||||
|
||||
const newRoles = updatedRoles.filter((newRole) => {
|
||||
return !roleNames.includes(newRole);
|
||||
});
|
||||
|
||||
try {
|
||||
const operator = new Operator(database);
|
||||
const data = await Client4.getRolesByNames(newRoles);
|
||||
await operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.ROLE,
|
||||
values: data,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
22
app/requests/remote/systems.ts
Normal file
22
app/requests/remote/systems.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {logError} from '@requests/remote/error';
|
||||
import {forceLogoutIfNecessary} from '@requests/remote/user';
|
||||
|
||||
export const getDataRetentionPolicy = async () => {
|
||||
let data = {};
|
||||
try {
|
||||
data = await Client4.getDataRetentionPolicy();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error);
|
||||
|
||||
//fixme: do we care for the below line ? It seems that the `error` object is never read ??
|
||||
// dispatch(batchActions([{type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, error,},]));
|
||||
logError(error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
386
app/requests/remote/user.ts
Normal file
386
app/requests/remote/user.ts
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import Operator from '@database/operator';
|
||||
import analytics from '@init/analytics';
|
||||
import {setServerCredentials} from '@init/credentials';
|
||||
import {getDeviceToken} from '@queries/global';
|
||||
import {getCommonSystemValues, getCurrentUserId} from '@queries/system';
|
||||
import {createSessions} from '@requests/local/systems';
|
||||
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@requests/local/timezone';
|
||||
import {logError} from '@requests/remote/error';
|
||||
import {loadRolesIfNeeded} from '@requests/remote/role';
|
||||
import {getDataRetentionPolicy} from '@requests/remote/systems';
|
||||
import {Client4Error} from '@typings/api/client4';
|
||||
import {Config} from '@typings/database/config';
|
||||
import {
|
||||
LoadMeArgs,
|
||||
LoginArgs,
|
||||
RawMyTeam,
|
||||
RawPreference,
|
||||
RawRole,
|
||||
RawTeam,
|
||||
RawTeamMembership,
|
||||
RawUser,
|
||||
} from '@typings/database/database';
|
||||
import {IsolatedEntities} from '@typings/database/enums';
|
||||
import {License} from '@typings/database/license';
|
||||
import Role from '@typings/database/role';
|
||||
import User from '@typings/database/user';
|
||||
import {createAndSetActiveDatabase, getActiveServerDatabase, getDefaultDatabase} from '@utils/database';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const logout = async (skipServerLogout = false) => {
|
||||
return async () => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
await Client4.logout();
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
//fixme: uncomment below EventEmitter.emit
|
||||
// EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
};
|
||||
|
||||
export const forceLogoutIfNecessary = async (err: Client4Error) => {
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
|
||||
if (!activeServerDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const currentUserId = await getCurrentUserId(activeServerDatabase);
|
||||
|
||||
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) {
|
||||
await logout(false);
|
||||
}
|
||||
|
||||
return {error: null};
|
||||
};
|
||||
|
||||
export const login = async ({ldapOnly = false, loginId, mfaToken, password}: LoginArgs) => {
|
||||
let deviceToken;
|
||||
let user;
|
||||
|
||||
const {error, defaultDatabase} = await getDefaultDatabase();
|
||||
if (!defaultDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const url = Client4.getUrl();
|
||||
|
||||
try {
|
||||
deviceToken = await getDeviceToken(defaultDatabase);
|
||||
user = ((await Client4.login(
|
||||
loginId,
|
||||
password,
|
||||
mfaToken,
|
||||
deviceToken,
|
||||
ldapOnly,
|
||||
)) as unknown) as RawUser;
|
||||
|
||||
await createAndSetActiveDatabase({serverUrl: url});
|
||||
await getCSRFFromCookie(Client4.getUrl());
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
const result = await loadMe({user, deviceToken});
|
||||
|
||||
if (!result?.error) {
|
||||
await completeLogin(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const loadMe = async ({deviceToken, user}: LoadMeArgs) => {
|
||||
let currentUser = user;
|
||||
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
if (!activeServerDatabase) {
|
||||
return {
|
||||
error,
|
||||
currentUser: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (deviceToken) {
|
||||
await Client4.attachDevice(deviceToken);
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
currentUser = ((await Client4.getMe()) as unknown) as RawUser;
|
||||
}
|
||||
} catch (e) {
|
||||
await forceLogoutIfNecessary(e);
|
||||
return {
|
||||
error: e,
|
||||
currentUser: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const analyticsClient = analytics.create(Client4.getUrl());
|
||||
analyticsClient.setUserId(currentUser.id);
|
||||
analyticsClient.setUserRoles(currentUser.roles);
|
||||
|
||||
//todo: Ask for a unified endpoint that will serve all those values in one go.( while ensuring backward-compatibility through fallbacks to previous code-path)
|
||||
const teamsRequest = Client4.getMyTeams();
|
||||
|
||||
// Goes into myTeam table
|
||||
const teamMembersRequest = Client4.getMyTeamMembers();
|
||||
const teamUnreadRequest = Client4.getMyTeamUnreads();
|
||||
|
||||
const preferencesRequest = Client4.getMyPreferences();
|
||||
const configRequest = Client4.getClientConfigOld();
|
||||
const licenseRequest = Client4.getClientLicenseOld();
|
||||
|
||||
const [
|
||||
teams,
|
||||
teamMembers,
|
||||
teamUnreads,
|
||||
preferences,
|
||||
config,
|
||||
license,
|
||||
] = await Promise.all([
|
||||
teamsRequest,
|
||||
teamMembersRequest,
|
||||
teamUnreadRequest,
|
||||
preferencesRequest,
|
||||
configRequest,
|
||||
licenseRequest,
|
||||
]);
|
||||
|
||||
const operator = new Operator(activeServerDatabase);
|
||||
|
||||
const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams: teams as RawTeam[]});
|
||||
|
||||
const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships: (teamMembers as unknown) as RawTeamMembership[]});
|
||||
|
||||
const myTeams = teamUnreads.map((unread) => {
|
||||
const matchingTeam = teamMembers.find((team) => team.team_id === unread.team_id);
|
||||
return {team_id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count};
|
||||
});
|
||||
|
||||
const myTeamRecords = operator.handleMyTeam({
|
||||
prepareRecordsOnly: true,
|
||||
myTeams: (myTeams as unknown) as RawMyTeam[],
|
||||
});
|
||||
|
||||
const systemRecords = operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values: [
|
||||
{
|
||||
name: 'config',
|
||||
value: config,
|
||||
},
|
||||
{
|
||||
name: 'license',
|
||||
value: license,
|
||||
},
|
||||
{
|
||||
name: 'currentUserId',
|
||||
value: currentUser.id,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
value: Client4.getUrl(),
|
||||
},
|
||||
],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const userRecords = operator.handleUsers({
|
||||
users: [currentUser],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const preferenceRecords = operator.handlePreferences({
|
||||
prepareRecordsOnly: true,
|
||||
preferences: (preferences as unknown) as RawPreference[],
|
||||
});
|
||||
|
||||
let roles: string[] = [];
|
||||
for (const role of currentUser.roles.split(' ')) {
|
||||
roles = roles.concat(role);
|
||||
}
|
||||
|
||||
for (const teamMember of teamMembers) {
|
||||
roles = roles.concat(teamMember.roles.split(' '));
|
||||
}
|
||||
|
||||
const rolesToLoad = new Set<string>(roles);
|
||||
|
||||
let rolesRecords: Role[] = [];
|
||||
if (rolesToLoad.size > 0) {
|
||||
const rolesByName = ((await Client4.getRolesByNames(Array.from(rolesToLoad))) as unknown) as RawRole[];
|
||||
|
||||
if (rolesByName?.length) {
|
||||
rolesRecords = operator.handleIsolatedEntity({tableName: IsolatedEntities.ROLE, prepareRecordsOnly: true, values: rolesByName}) as Role[];
|
||||
}
|
||||
}
|
||||
|
||||
const models = await Promise.all([teamRecords, teamMembershipRecords, myTeamRecords, systemRecords, preferenceRecords, rolesRecords, userRecords]);
|
||||
|
||||
const flattenedModels = models.flat();
|
||||
if (flattenedModels?.length > 0) {
|
||||
await operator.batchOperations({database: activeServerDatabase, models: flattenedModels});
|
||||
}
|
||||
} catch (e) {
|
||||
return {error: e, currentUser: undefined};
|
||||
}
|
||||
|
||||
return {currentUser, error: undefined};
|
||||
};
|
||||
|
||||
export const completeLogin = async (user: RawUser) => {
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
if (!activeServerDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const {config, license}: { config: Partial<Config>; license: Partial<License>; } = await getCommonSystemValues(activeServerDatabase);
|
||||
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = Client4.getToken();
|
||||
const serverUrl = Client4.getUrl();
|
||||
|
||||
setServerCredentials(serverUrl, user.id, token);
|
||||
|
||||
// Set timezone
|
||||
if (isTimezoneEnabled(config)) {
|
||||
const timezone = getDeviceTimezone();
|
||||
await autoUpdateTimezone({deviceTimezone: timezone, userId: user.id});
|
||||
}
|
||||
|
||||
let dataRetentionPolicy: any;
|
||||
const operator = new Operator(activeServerDatabase);
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
dataRetentionPolicy = await getDataRetentionPolicy();
|
||||
await operator.handleIsolatedEntity({tableName: IsolatedEntities.SYSTEM, values: [{name: 'dataRetentionPolicy', value: dataRetentionPolicy}], prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateMe = async (user: User) => {
|
||||
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||
if (!activeServerDatabase) {
|
||||
return {error};
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = ((await Client4.patchMe(user._raw)) as unknown) as RawUser;
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
const operator = new Operator(activeServerDatabase);
|
||||
const systemRecords = operator.handleIsolatedEntity({
|
||||
tableName: IsolatedEntities.SYSTEM,
|
||||
values: [
|
||||
{name: 'currentUserId', value: data.id},
|
||||
{name: 'locale', value: data?.locale},
|
||||
],
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
const userRecord = operator.handleUsers({prepareRecordsOnly: true, users: [data]});
|
||||
|
||||
//todo: ?? Do we need to write to TOS entity ? See app/mm-redux/reducers/entities/users.ts/profiles/line 152 const
|
||||
// tosRecords = await DataOperator.handleIsolatedEntity({ tableName: TERMS_OF_SERVICE, values: [{}], });
|
||||
const models = await Promise.all([
|
||||
...systemRecords,
|
||||
...userRecord,
|
||||
|
||||
// ...tosRecords,
|
||||
]);
|
||||
|
||||
if (models?.length) {
|
||||
await operator.batchOperations({database: activeServerDatabase, models: models.flat()});
|
||||
}
|
||||
|
||||
const updatedRoles: string[] = data.roles.split(' ');
|
||||
if (updatedRoles.length) {
|
||||
await loadRolesIfNeeded(updatedRoles);
|
||||
}
|
||||
|
||||
return {data};
|
||||
};
|
||||
|
||||
export const getSessions = async (currentUserId: string) => {
|
||||
try {
|
||||
const sessions = await Client4.getSessions(currentUserId);
|
||||
await createSessions(sessions);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await forceLogoutIfNecessary(e);
|
||||
}
|
||||
};
|
||||
|
||||
type LoadedUser = {
|
||||
currentUser?: RawUser,
|
||||
error?: Client4Error
|
||||
}
|
||||
|
||||
export const ssoLogin = async (serverUrl: string) => {
|
||||
let deviceToken;
|
||||
|
||||
const {error, defaultDatabase} = await getDefaultDatabase();
|
||||
|
||||
if (!defaultDatabase) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
// Setting up active database for this SSO login flow
|
||||
try {
|
||||
await createAndSetActiveDatabase({serverUrl});
|
||||
deviceToken = await getDeviceToken(defaultDatabase);
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = await loadMe({deviceToken}) as unknown as LoadedUser;
|
||||
if (!result?.error && result?.currentUser) {
|
||||
await completeLogin(result.currentUser);
|
||||
}
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (email: string) => {
|
||||
let data;
|
||||
try {
|
||||
data = await Client4.sendPasswordResetEmail(email);
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data,
|
||||
error: undefined,
|
||||
};
|
||||
};
|
||||
|
|
@ -11,9 +11,27 @@ import {
|
|||
ReloadInstructions,
|
||||
} from 'react-native/Libraries/NewAppScreen';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
|
||||
declare const global: {HermesInternal: null | {}};
|
||||
|
||||
const Channel = () => {
|
||||
type ChannelProps = LaunchProps
|
||||
|
||||
const Channel = (props: ChannelProps) => {
|
||||
// TODO: If we have LaunchProps, ensure we load the correct channel/post/modal.
|
||||
const {launchType} = props;
|
||||
console.log(launchType); // eslint-disable-line no-console
|
||||
// TODO: If LaunchProps.error is true, use the LaunchProps.launchType to determine which
|
||||
// error message to display. For example:
|
||||
// if (props.launchError) {
|
||||
// let erroMessage;
|
||||
// if (props.launchType === LaunchType.DeepLink) {
|
||||
// errorMessage = intl.formatMessage({id: 'mobile.launchError.deepLink', defaultMessage: 'Did not find a server for this deep link'});
|
||||
// } else if (props.launchType === LaunchType.Notification) {
|
||||
// errorMessage = intl.formatMessage({id: 'mobile.launchError.notification', defaultMessage: 'Did not find a server for this notification'});
|
||||
// }
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusBar barStyle='dark-content'/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ForgotPassword should match snapshot 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.screen"
|
||||
>
|
||||
<View
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"paddingHorizontal": 15,
|
||||
"paddingVertical": 50,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/logo.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 72,
|
||||
"resizeMode": "contain",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#fd5960",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
"textAlign": "left",
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="forgot.password.error.text"
|
||||
>
|
||||
|
||||
</Text>
|
||||
<View
|
||||
testID="password_send.link.prepare"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.6)",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "300",
|
||||
"lineHeight": 22,
|
||||
"marginBottom": 15,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {
|
||||
"paddingTop": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
To reset your password, enter the email address you used to sign up
|
||||
</Text>
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType="email-address"
|
||||
onChangeText={[Function]}
|
||||
placeholder="Email"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
rejectResponderTermination={true}
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.email"
|
||||
underlineColorAndroid="transparent"
|
||||
/>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
focusable={false}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "#166de0",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 10,
|
||||
"opacity": 1,
|
||||
"padding": 15,
|
||||
}
|
||||
}
|
||||
testID="forgot.password.button"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#166de0",
|
||||
"fontSize": 17,
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
Reset my password
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
52
app/screens/forgot_password/forgot_password.test.tsx
Normal file
52
app/screens/forgot_password/forgot_password.test.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {act, waitFor} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import * as UserAPICalls from '@requests/remote/user';
|
||||
import {renderWithIntl, fireEvent} from '@test/intl-test-helper';
|
||||
|
||||
import ForgotPassword from './index';
|
||||
|
||||
describe('ForgotPassword', () => {
|
||||
const baseProps = {
|
||||
componentId: 'ForgotPassword',
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const {toJSON} = renderWithIntl(<ForgotPassword {...baseProps}/>);
|
||||
expect(toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Error on failure of email regex', async () => {
|
||||
const {getByTestId} = renderWithIntl(<ForgotPassword {...baseProps}/>);
|
||||
const emailTextInput = getByTestId('forgot.password.email');
|
||||
const resetButton = getByTestId('forgot.password.button');
|
||||
|
||||
fireEvent.changeText(emailTextInput, 'bar');
|
||||
|
||||
act(() => {
|
||||
fireEvent.press(resetButton);
|
||||
});
|
||||
|
||||
expect(getByTestId('forgot.password.error.text')).toBeDefined();
|
||||
});
|
||||
|
||||
test('Should show password link sent texts', async () => {
|
||||
const spyOnResetAPICall = jest.spyOn(UserAPICalls, 'sendPasswordResetEmail');
|
||||
const {getByTestId} = renderWithIntl(<ForgotPassword {...baseProps}/>);
|
||||
const emailTextInput = getByTestId('forgot.password.email');
|
||||
const resetButton = getByTestId('forgot.password.button');
|
||||
|
||||
fireEvent.changeText(emailTextInput, 'test@test.com');
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.press(resetButton);
|
||||
});
|
||||
|
||||
expect(spyOnResetAPICall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
223
app/screens/forgot_password/index.tsx
Normal file
223
app/screens/forgot_password/index.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Image, Text, TextInput, TouchableWithoutFeedback, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import ErrorText from '@components/error_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {sendPasswordResetEmail} from '@requests/remote/user';
|
||||
import {isEmail} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const ForgotPassword = ({theme}: Props) => {
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isPasswordLinkSent, setIsPasswordLinkSent] = useState<boolean>(false);
|
||||
const {formatMessage} = useIntl();
|
||||
const emailIdRef = useRef<TextInput>(null);
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const changeEmail = (emailAddress: string) => {
|
||||
setEmail(emailAddress);
|
||||
};
|
||||
|
||||
const submitResetPassword = async () => {
|
||||
if (!isEmail(email)) {
|
||||
setError(
|
||||
formatMessage({
|
||||
id: 'password_send.error',
|
||||
defaultMessage: 'Please enter a valid email address.',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const {data, error: apiError = undefined} = await sendPasswordResetEmail(email);
|
||||
|
||||
if (data) {
|
||||
setIsPasswordLinkSent(true);
|
||||
}
|
||||
|
||||
setError(apiError);
|
||||
};
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
emailIdRef.current?.blur();
|
||||
}, []);
|
||||
|
||||
const getDisplayErrorView = () => {
|
||||
return (
|
||||
<ErrorText
|
||||
error={error}
|
||||
testID='forgot.password.error.text'
|
||||
textStyle={styles.errorText}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getCenterContent = () => {
|
||||
if (isPasswordLinkSent) {
|
||||
return (
|
||||
<View
|
||||
style={styles.resetSuccessContainer}
|
||||
testID={'password_send.link.sent'}
|
||||
>
|
||||
<FormattedText
|
||||
style={styles.successTxtColor}
|
||||
id='password_send.link'
|
||||
defaultMessage='If the account exists, a password reset email will be sent to:'
|
||||
/>
|
||||
<Text style={[styles.successTxtColor, styles.emailId]}>
|
||||
{email}
|
||||
</Text>
|
||||
<FormattedText
|
||||
style={[
|
||||
styles.successTxtColor,
|
||||
styles.defaultTopPadding,
|
||||
]}
|
||||
id='password_send.checkInbox'
|
||||
defaultMessage='Please check your inbox.'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID={'password_send.link.prepare'}>
|
||||
<FormattedText
|
||||
style={[styles.subheader, styles.defaultTopPadding]}
|
||||
id='password_send.description'
|
||||
defaultMessage='To reset your password, enter the email address you used to sign up'
|
||||
/>
|
||||
<TextInput
|
||||
ref={emailIdRef}
|
||||
style={styles.inputBox}
|
||||
onChangeText={changeEmail}
|
||||
placeholder={formatMessage({
|
||||
id: 'login.email',
|
||||
defaultMessage: 'Email',
|
||||
})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
autoCorrect={false}
|
||||
autoCapitalize='none'
|
||||
keyboardType='email-address'
|
||||
underlineColorAndroid='transparent'
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
testID={'forgot.password.email'}
|
||||
/>
|
||||
<Button
|
||||
testID='forgot.password.button'
|
||||
containerStyle={styles.signupButton}
|
||||
disabled={!email}
|
||||
onPress={submitResetPassword}
|
||||
>
|
||||
<FormattedText
|
||||
id='password_send.reset'
|
||||
defaultMessage='Reset my password'
|
||||
style={styles.signupButtonText}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
testID='forgot.password.screen'
|
||||
style={styles.container}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={onBlur}>
|
||||
<View style={styles.innerContainer}>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={styles.innerContainerImage}
|
||||
/>
|
||||
{getDisplayErrorView()}
|
||||
{getCenterContent()}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
innerContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 50,
|
||||
},
|
||||
forgotPasswordBtn: {
|
||||
borderColor: 'transparent',
|
||||
marginTop: 15,
|
||||
},
|
||||
resetSuccessContainer: {
|
||||
marginTop: 15,
|
||||
padding: 10,
|
||||
backgroundColor: '#dff0d8',
|
||||
borderColor: '#d6e9c6',
|
||||
},
|
||||
emailId: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
successTxtColor: {
|
||||
color: '#3c763d',
|
||||
},
|
||||
defaultTopPadding: {
|
||||
paddingTop: 15,
|
||||
},
|
||||
subheader: {
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '300',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
marginBottom: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
inputBox: {
|
||||
fontSize: 16,
|
||||
height: 45,
|
||||
borderColor: 'gainsboro',
|
||||
borderWidth: 1,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
paddingLeft: 10,
|
||||
alignSelf: 'stretch',
|
||||
borderRadius: 3,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
signupButton: {
|
||||
borderRadius: 3,
|
||||
borderColor: theme.buttonBg,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
marginTop: 10,
|
||||
padding: 15,
|
||||
},
|
||||
signupButtonText: {
|
||||
textAlign: 'center',
|
||||
color: theme.buttonBg,
|
||||
fontSize: 17,
|
||||
},
|
||||
innerContainerImage: {
|
||||
height: 72,
|
||||
resizeMode: 'contain',
|
||||
},
|
||||
}));
|
||||
|
||||
export default ForgotPassword;
|
||||
|
|
@ -2,18 +2,20 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
|
||||
import {Platform, StyleProp, ViewStyle} from 'react-native';
|
||||
import {withManagedConfig} from '@mattermost/react-native-emm';
|
||||
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
|
||||
|
||||
import {withManagedConfig} from '@mattermost/react-native-emm';
|
||||
|
||||
import {Screens} from '@constants';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
|
||||
// TODO: Remove this and uncomment screens as they get added
|
||||
/* eslint-disable */
|
||||
|
||||
const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewStyle>) => {
|
||||
const withGestures = (screen: NavigationFunctionComponent, styles: StyleProp<ViewStyle>) => {
|
||||
if (Platform.OS === 'android') {
|
||||
return gestureHandlerRootHOC(screen, styles);
|
||||
}
|
||||
|
|
@ -21,10 +23,23 @@ const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewSt
|
|||
return screen;
|
||||
};
|
||||
|
||||
const withIntl = (Screen: React.ComponentType) => {
|
||||
return function IntlEnabledComponent(props: any) {
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={DEFAULT_LOCALE}
|
||||
messages={getTranslations()}
|
||||
>
|
||||
<Screen {...props}/>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
// let screen: any;
|
||||
// let extraStyles: StyleProp<ViewStyle>;
|
||||
// switch (screenName) {
|
||||
let screen: any|undefined;
|
||||
let extraStyles: StyleProp<ViewStyle>;
|
||||
switch (screenName) {
|
||||
// case 'About':
|
||||
// screen = require('@screens/about').default;
|
||||
// break;
|
||||
|
|
@ -79,30 +94,30 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
// case 'FlaggedPosts':
|
||||
// screen = require('@screens/flagged_posts').default;
|
||||
// break;
|
||||
// case 'ForgotPassword':
|
||||
// screen = require('@screens/forgot_password').default;
|
||||
// break;
|
||||
case 'ForgotPassword':
|
||||
screen = require('@screens/forgot_password').default;
|
||||
break;
|
||||
// case 'Gallery':
|
||||
// screen = require('@screens/gallery').default;
|
||||
// break;
|
||||
// case 'InteractiveDialog':
|
||||
// screen = require('@screens/interactive_dialog').default;
|
||||
// break;
|
||||
// case 'Login':
|
||||
// screen = require('@screens/login').default;
|
||||
// break;
|
||||
// case 'LoginOptions':
|
||||
// screen = require('@screens/login_options').default;
|
||||
// break;
|
||||
case 'Login':
|
||||
screen = require('@screens/login').default;
|
||||
break;
|
||||
case 'LoginOptions':
|
||||
screen = require('@screens/login_options').default;
|
||||
break;
|
||||
// case 'LongPost':
|
||||
// screen = require('@screens/long_post').default;
|
||||
// break;
|
||||
// case 'MainSidebar':
|
||||
// screen = require('app/components/sidebars/main').default;
|
||||
// break;
|
||||
// case 'MFA':
|
||||
// screen = require('@screens/mfa').default;
|
||||
// break;
|
||||
case 'MFA':
|
||||
screen = require('@screens/mfa').default;
|
||||
break;
|
||||
// case 'MoreChannels':
|
||||
// screen = require('@screens/more_channels').default;
|
||||
// break;
|
||||
|
|
@ -170,9 +185,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
// case 'SidebarSettings':
|
||||
// screen = require('@screens/settings/sidebar').default;
|
||||
// break;
|
||||
// case 'SSO':
|
||||
// screen = require('@screens/sso').default;
|
||||
// break;
|
||||
case 'SSO':
|
||||
screen = require('@screens/sso').default;
|
||||
break;
|
||||
// case 'Table':
|
||||
// screen = require('@screens/table').default;
|
||||
// break;
|
||||
|
|
@ -191,11 +206,11 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
// case 'UserProfile':
|
||||
// screen = require('@screens/user_profile').default;
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
|
||||
// if (screen) {
|
||||
// Navigation.registerComponent(screenName, () => withGestures(withManagedConfig(screen), extraStyles));
|
||||
// }
|
||||
if (screen) {
|
||||
Navigation.registerComponent(screenName, () => withGestures(withIntl(withManagedConfig(screen)), extraStyles));
|
||||
}
|
||||
});
|
||||
|
||||
export function registerScreens() {
|
||||
|
|
@ -203,5 +218,5 @@ export function registerScreens() {
|
|||
const serverScreen = require('@screens/server').default;
|
||||
|
||||
Navigation.registerComponent(Screens.CHANNEL, () => withManagedConfig(channelScreen));
|
||||
Navigation.registerComponent(Screens.SERVER, () => withManagedConfig(serverScreen));
|
||||
Navigation.registerComponent(Screens.SERVER, () => withIntl(withManagedConfig(serverScreen)));
|
||||
}
|
||||
213
app/screens/login/__snapshots__/login.test.tsx.snap
Normal file
213
app/screens/login/__snapshots__/login.test.tsx.snap
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Login Login screen should match snapshot 1`] = `
|
||||
<RCTSafeAreaView
|
||||
emulateUnlessSupported={true}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<RCTScrollView
|
||||
accessible={false}
|
||||
automaticallyAdjustContentInsets={false}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "column",
|
||||
"justifyContent": "center",
|
||||
"paddingHorizontal": 15,
|
||||
"paddingVertical": 50,
|
||||
}
|
||||
}
|
||||
contentInset={
|
||||
Object {
|
||||
"bottom": 0,
|
||||
}
|
||||
}
|
||||
enableAutomaticScroll={true}
|
||||
enableOnAndroid={true}
|
||||
enableResetScrollToCoords={true}
|
||||
extraHeight={75}
|
||||
extraScrollHeight={0}
|
||||
focusable={true}
|
||||
getScrollResponder={[Function]}
|
||||
handleOnScroll={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardOpeningTime={250}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardSpace={0}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onScroll={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
resetKeyboardSpace={[Function]}
|
||||
scrollEventThrottle={1}
|
||||
scrollForExtraHeightOnAndroid={[Function]}
|
||||
scrollIntoView={[Function]}
|
||||
scrollToEnd={[Function]}
|
||||
scrollToFocusedInput={[Function]}
|
||||
scrollToPosition={[Function]}
|
||||
showsVerticalScrollIndicator={true}
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
update={[Function]}
|
||||
viewIsInsideTabBar={false}
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/logo.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 72,
|
||||
"resizeMode": "contain",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType="email-address"
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder="Email or Username"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
rejectResponderTermination={true}
|
||||
returnKeyType="next"
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="login.username.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
onChangeText={[Function]}
|
||||
onSubmitEditing={[Function]}
|
||||
placeholder="Password"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
rejectResponderTermination={true}
|
||||
returnKeyType="go"
|
||||
secureTextEntry={true}
|
||||
style={
|
||||
Object {
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "gainsboro",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 16,
|
||||
"height": 45,
|
||||
"marginBottom": 5,
|
||||
"marginTop": 5,
|
||||
"paddingLeft": 10,
|
||||
}
|
||||
}
|
||||
testID="login.password.input"
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"alignSelf": "stretch",
|
||||
"borderColor": "#166de0",
|
||||
"borderRadius": 3,
|
||||
"borderWidth": 1,
|
||||
"marginTop": 10,
|
||||
"opacity": 1,
|
||||
"padding": 15,
|
||||
}
|
||||
}
|
||||
testID="login.signin.button"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#166de0",
|
||||
"fontSize": 17,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
Sign in
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"borderColor": "transparent",
|
||||
"marginTop": 15,
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#2389d7",
|
||||
}
|
||||
}
|
||||
testID="login.forgot"
|
||||
>
|
||||
I forgot my password
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</RCTScrollView>
|
||||
</RCTSafeAreaView>
|
||||
`;
|
||||
452
app/screens/login/index.tsx
Normal file
452
app/screens/login/index.tsx
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
StyleProp,
|
||||
Text,
|
||||
TextInput,
|
||||
TextStyle,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {NavigationFunctionComponent} from 'react-native-navigation';
|
||||
|
||||
import ErrorText, {ClientErrorWithIntl} from '@components/error_text';
|
||||
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {scheduleExpiredNotification} from '@requests/remote/push_notification';
|
||||
import {login} from '@requests/remote/user';
|
||||
import {goToScreen, resetToChannel} from '@screens/navigation';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type LoginProps = {
|
||||
componentId: string;
|
||||
config: ClientConfig;
|
||||
license: ClientLicense;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error'];
|
||||
|
||||
const Login: NavigationFunctionComponent = ({config, license, theme}: LoginProps) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
const loginRef = useRef<TextInput>(null);
|
||||
const passwordRef = useRef<TextInput>(null);
|
||||
const scrollRef = useRef<KeyboardAwareScrollView>(null);
|
||||
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<ClientErrorWithIntl | string | undefined | null>();
|
||||
|
||||
const [loginId, setLoginId] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
|
||||
// useEffect to set userName for EMM
|
||||
useEffect(() => {
|
||||
const setEmmUsernameIfAvailable = async () => {
|
||||
if (managedConfig?.username && loginRef.current) {
|
||||
loginRef.current.setNativeProps({text: managedConfig.username});
|
||||
setLoginId(managedConfig.username);
|
||||
}
|
||||
};
|
||||
|
||||
setEmmUsernameIfAvailable();
|
||||
}, []);
|
||||
|
||||
const preSignIn = preventDoubleTap(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
Keyboard.dismiss();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
if (!loginId) {
|
||||
t('login.noEmail');
|
||||
t('login.noEmailLdapUsername');
|
||||
t('login.noEmailUsername');
|
||||
t('login.noEmailUsernameLdapUsername');
|
||||
t('login.noLdapUsername');
|
||||
t('login.noUsername');
|
||||
t('login.noUsernameLdapUsername');
|
||||
|
||||
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
|
||||
let msgId = 'login.no';
|
||||
if (config.EnableSignInWithEmail === 'true') {
|
||||
msgId += 'Email';
|
||||
}
|
||||
if (config.EnableSignInWithUsername === 'true') {
|
||||
msgId += 'Username';
|
||||
}
|
||||
if (license.IsLicensed === 'true' && config.EnableLdap === 'true') {
|
||||
msgId += 'LdapUsername';
|
||||
}
|
||||
|
||||
const ldapUsername = intl.formatMessage({
|
||||
id: 'login.ldapUsernameLower',
|
||||
defaultMessage: 'AD/LDAP username',
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
setError(intl.formatMessage(
|
||||
{
|
||||
id: msgId,
|
||||
defaultMessage: '',
|
||||
},
|
||||
{
|
||||
ldapUsername: config.LdapLoginFieldName || ldapUsername,
|
||||
},
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setIsLoading(false);
|
||||
setError(intl.formatMessage({
|
||||
id: t('login.noPassword'),
|
||||
defaultMessage: 'Please enter your password',
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
signIn();
|
||||
});
|
||||
});
|
||||
|
||||
const signIn = async () => {
|
||||
const result = await login({loginId: loginId.toLowerCase(), password, config, license});
|
||||
if (checkLoginResponse(result)) {
|
||||
await goToChannel();
|
||||
}
|
||||
};
|
||||
|
||||
const goToChannel = async () => {
|
||||
await scheduleExpiredNotification(intl);
|
||||
resetToChannel();
|
||||
};
|
||||
|
||||
const checkLoginResponse = (data: any) => {
|
||||
if (MFA_EXPECTED_ERRORS.includes(data?.error?.server_error_id)) {
|
||||
goToMfa();
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data?.error) {
|
||||
setIsLoading(false);
|
||||
setError(getLoginErrorMessage(data.error));
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const goToMfa = () => {
|
||||
const screen = MFA;
|
||||
const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'});
|
||||
goToScreen(screen, title, {goToChannel, loginId, password, config, license, theme});
|
||||
};
|
||||
|
||||
const getLoginErrorMessage = (loginError: any) => {
|
||||
return (getServerErrorForLogin(loginError) || loginError);
|
||||
};
|
||||
|
||||
const getServerErrorForLogin = (serverError: any) => {
|
||||
if (!serverError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorId = serverError.server_error_id;
|
||||
|
||||
if (!errorId) {
|
||||
return serverError.message;
|
||||
}
|
||||
|
||||
if (errorId === 'store.sql_user.get_for_login.app_error' || errorId === 'ent.ldap.do_login.user_not_registered.app_error') {
|
||||
return {
|
||||
intl: {
|
||||
id: t('login.userNotFound'),
|
||||
defaultMessage: 'We couldn\'t find an account matching your login credentials.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (errorId === 'api.user.check_user_password.invalid.app_error' || errorId === 'ent.ldap.do_login.invalid_password.app_error') {
|
||||
return {
|
||||
intl: {
|
||||
id: t('login.invalidPassword'),
|
||||
defaultMessage: 'Your password is incorrect.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return serverError.message;
|
||||
};
|
||||
|
||||
const onPressForgotPassword = () => {
|
||||
const screen = FORGOT_PASSWORD;
|
||||
const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'});
|
||||
|
||||
goToScreen(screen, title, {theme});
|
||||
};
|
||||
|
||||
const createLoginPlaceholder = () => {
|
||||
const {formatMessage} = intl;
|
||||
const loginPlaceholders = [];
|
||||
|
||||
if (config.EnableSignInWithEmail === 'true') {
|
||||
loginPlaceholders.push(formatMessage({id: 'login.email', defaultMessage: 'Email'}));
|
||||
}
|
||||
|
||||
if (config.EnableSignInWithUsername === 'true') {
|
||||
loginPlaceholders.push(formatMessage({id: 'login.username', defaultMessage: 'Username'}));
|
||||
}
|
||||
|
||||
if (license.IsLicensed === 'true' && license.LDAP === 'true' && config.EnableLdap === 'true') {
|
||||
if (config.LdapLoginFieldName) {
|
||||
loginPlaceholders.push(config.LdapLoginFieldName);
|
||||
} else {
|
||||
loginPlaceholders.push(formatMessage({id: 'login.ldapUsername', defaultMessage: 'AD/LDAP Username'}));
|
||||
}
|
||||
}
|
||||
|
||||
if (loginPlaceholders.length >= 2) {
|
||||
return loginPlaceholders.slice(0, loginPlaceholders.length - 1).join(', ') +
|
||||
` ${formatMessage({id: 'login.or', defaultMessage: 'or'})} ` +
|
||||
loginPlaceholders[loginPlaceholders.length - 1];
|
||||
}
|
||||
|
||||
if (loginPlaceholders.length === 1) {
|
||||
return loginPlaceholders[0];
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
loginRef?.current?.blur();
|
||||
passwordRef?.current?.blur();
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
const onLoginChange = useCallback((text) => {
|
||||
setLoginId(text);
|
||||
}, []);
|
||||
|
||||
const onPasswordChange = useCallback((text) => {
|
||||
setPassword(text);
|
||||
}, []);
|
||||
|
||||
const onPasswordFocus = useCallback(() => {
|
||||
passwordRef?.current?.focus();
|
||||
}, []);
|
||||
|
||||
// **** **** **** RENDER METHOD **** **** ****
|
||||
|
||||
const renderProceedButton = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
animating={true}
|
||||
size='small'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const additionalStyle: StyleProp<ViewStyle> = {
|
||||
...(config.EmailLoginButtonColor && {
|
||||
backgroundColor: config.EmailLoginButtonColor,
|
||||
}),
|
||||
...(config.EmailLoginButtonBorderColor && {
|
||||
borderColor: config.EmailLoginButtonBorderColor,
|
||||
}),
|
||||
};
|
||||
|
||||
const additionalTextStyle: StyleProp<TextStyle> = {
|
||||
...(config.EmailLoginButtonTextColor && {
|
||||
color: config.EmailLoginButtonTextColor,
|
||||
}),
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
testID='login.signin.button'
|
||||
onPress={preSignIn}
|
||||
containerStyle={[styles.signupButton, additionalStyle]}
|
||||
>
|
||||
<FormattedText
|
||||
id='login.signIn'
|
||||
defaultMessage='Sign in'
|
||||
style={[styles.signupButtonText, additionalTextStyle]}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar/>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onBlur}
|
||||
accessible={false}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
ref={scrollRef}
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.innerContainer}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
enableOnAndroid={true}
|
||||
>
|
||||
<Image
|
||||
source={require('@assets/images/logo.png')}
|
||||
style={{height: 72, resizeMode: 'contain'}}
|
||||
/>
|
||||
{config?.SiteName && (<View testID='login.screen'>
|
||||
<Text style={styles.header}>{config?.SiteName}</Text>
|
||||
<FormattedText
|
||||
style={styles.subheader}
|
||||
id='web.root.signup_info'
|
||||
defaultMessage='All team communication in one place, searchable and accessible anywhere'
|
||||
/>
|
||||
</View>)}
|
||||
{error && (
|
||||
<ErrorText
|
||||
testID='login.error.text'
|
||||
error={error}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
testID='login.username.input'
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
disableFullscreenUI={true}
|
||||
keyboardType='email-address'
|
||||
onChangeText={onLoginChange}
|
||||
onSubmitEditing={onPasswordFocus}
|
||||
placeholder={createLoginPlaceholder()}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={loginRef}
|
||||
returnKeyType='next'
|
||||
style={styles.inputBox}
|
||||
underlineColorAndroid='transparent'
|
||||
value={loginId} //to remove
|
||||
/>
|
||||
<TextInput
|
||||
testID='login.password.input'
|
||||
autoCapitalize='none'
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
onChangeText={onPasswordChange}
|
||||
onSubmitEditing={preSignIn}
|
||||
style={styles.inputBox}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'login.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
ref={passwordRef}
|
||||
returnKeyType='go'
|
||||
secureTextEntry={true}
|
||||
underlineColorAndroid='transparent'
|
||||
value={password} //to remove
|
||||
/>
|
||||
{renderProceedButton()}
|
||||
{(config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') && (
|
||||
<Button
|
||||
onPress={onPressForgotPassword}
|
||||
containerStyle={[styles.forgotPasswordBtn]}
|
||||
>
|
||||
<FormattedText
|
||||
id='login.forgot'
|
||||
defaultMessage='I forgot my password'
|
||||
style={styles.forgotPasswordTxt}
|
||||
testID={'login.forgot'}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
innerContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 15,
|
||||
paddingVertical: 50,
|
||||
},
|
||||
forgotPasswordBtn: {
|
||||
borderColor: 'transparent',
|
||||
marginTop: 15,
|
||||
},
|
||||
forgotPasswordTxt: {
|
||||
color: theme.linkColor,
|
||||
},
|
||||
inputBox: {
|
||||
fontSize: 16,
|
||||
height: 45,
|
||||
borderColor: 'gainsboro',
|
||||
borderWidth: 1,
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
paddingLeft: 10,
|
||||
alignSelf: 'stretch',
|
||||
borderRadius: 3,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
subheader: {
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: '300',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
marginBottom: 15,
|
||||
lineHeight: 22,
|
||||
},
|
||||
signupButton: {
|
||||
borderRadius: 3,
|
||||
borderColor: theme.buttonBg,
|
||||
borderWidth: 1,
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
marginTop: 10,
|
||||
padding: 15,
|
||||
},
|
||||
signupButtonText: {
|
||||
textAlign: 'center',
|
||||
color: theme.buttonBg,
|
||||
fontSize: 17,
|
||||
},
|
||||
header: {
|
||||
textAlign: 'center',
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
fontSize: 32,
|
||||
fontWeight: '600',
|
||||
},
|
||||
}));
|
||||
|
||||
export default Login;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue