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
|
insert_final_newline = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
||||||
[*.{js,jsx,json,html}]
|
[*.{js,jsx,json,html,ts,tsx}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:mattermost/react",
|
"plugin:mattermost/react",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -101,3 +101,4 @@ detox/artifacts
|
||||||
|
|
||||||
#editor-settings
|
#editor-settings
|
||||||
.vscode
|
.vscode
|
||||||
|
.scannerwork
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mattermost.rnbeta">
|
||||||
package="com.mattermost.rnbeta">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
@ -44,17 +43,16 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="mattermost" />
|
<data android:scheme="mattermost" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
<service android:name=".NotificationDismissService"
|
<service android:name=".NotificationDismissService" android:enabled="true" android:exported="false" />
|
||||||
android:enabled="true"
|
<receiver android:name=".NotificationReplyBroadcastReceiver" android:enabled="true" android:exported="false" />
|
||||||
android:exported="false" />
|
<activity android:name="com.reactnativenavigation.controllers.NavigationActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:resizeableActivity="true"/>
|
||||||
<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
|
<activity
|
||||||
android:name="com.mattermost.share.ShareActivity"
|
android:name="com.mattermost.share.ShareActivity"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
package com.mattermost.helpers;
|
package com.mattermost.helpers;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.Arguments;
|
import com.facebook.react.bridge.Arguments;
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.WritableMap;
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
|
@ -10,27 +7,9 @@ import com.oblador.keychain.KeychainModule;
|
||||||
|
|
||||||
|
|
||||||
public class Credentials {
|
public class Credentials {
|
||||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
public static void getCredentialsForServer(ReactApplicationContext context, String serverUrl, ResolvePromise promise) {
|
||||||
|
|
||||||
public static void getCredentialsForCurrentServer(ReactApplicationContext context, ResolvePromise promise) {
|
|
||||||
final KeychainModule keychainModule = new KeychainModule(context);
|
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();
|
final WritableMap options = Arguments.createMap();
|
||||||
// KeyChain module fails if `authenticationPrompt` is not set
|
// KeyChain module fails if `authenticationPrompt` is not set
|
||||||
final WritableMap authPrompt = Arguments.createMap();
|
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.List;
|
||||||
import java.util.Map;
|
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.notification.PushNotification;
|
||||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
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 static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||||
|
|
||||||
import com.mattermost.helpers.ResolvePromise;
|
|
||||||
|
|
||||||
public class CustomPushNotification extends PushNotification {
|
public class CustomPushNotification extends PushNotification {
|
||||||
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
public static final int MESSAGE_NOTIFICATION_ID = 435345;
|
||||||
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
|
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 NOTIFICATION_ID = "notificationId";
|
||||||
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
|
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
|
||||||
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
|
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) {
|
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
createNotificationChannels();
|
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;
|
final boolean isIdLoaded = initialData.getString("id_loaded") != null ? initialData.getString("id_loaded").equals("true") : false;
|
||||||
int notificationId = MESSAGE_NOTIFICATION_ID;
|
int notificationId = MESSAGE_NOTIFICATION_ID;
|
||||||
|
|
||||||
if (ackId != null) {
|
String serverUrl = initialData.getString("server_url", DatabaseHelper.getOnlyServerUrl(context));
|
||||||
notificationReceiptDelivery(ackId, postId, type, isIdLoaded, new ResolvePromise() {
|
|
||||||
|
if (ackId != null && serverUrl != null) {
|
||||||
|
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
|
||||||
@Override
|
@Override
|
||||||
public void resolve(@Nullable Object value) {
|
public void resolve(@Nullable Object value) {
|
||||||
if (isIdLoaded) {
|
if (isIdLoaded) {
|
||||||
|
|
@ -127,6 +131,11 @@ public class CustomPushNotification extends PushNotification {
|
||||||
// so we fetch the bundle again
|
// so we fetch the bundle again
|
||||||
final Bundle data = mNotificationProps.asBundle();
|
final Bundle data = mNotificationProps.asBundle();
|
||||||
|
|
||||||
|
if (serverUrl == null) {
|
||||||
|
String message = data.getString("message");
|
||||||
|
data.putString("message", "Unknown Server\n" + message);
|
||||||
|
}
|
||||||
|
|
||||||
if (channelId != null) {
|
if (channelId != null) {
|
||||||
notificationId = channelId.hashCode();
|
notificationId = channelId.hashCode();
|
||||||
|
|
||||||
|
|
@ -465,15 +474,18 @@ public class CustomPushNotification extends PushNotification {
|
||||||
|
|
||||||
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
|
private void addNotificationReplyAction(Notification.Builder notification, int notificationId, Bundle bundle) {
|
||||||
String postId = bundle.getString("post_id");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
Intent replyIntent = new Intent(mContext, NotificationReplyBroadcastReceiver.class);
|
||||||
replyIntent.setAction(KEY_TEXT_REPLY);
|
replyIntent.setAction(KEY_TEXT_REPLY);
|
||||||
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
|
||||||
replyIntent.putExtra("pushNotification", bundle);
|
replyIntent.putExtra(NOTIFICATION, bundle);
|
||||||
|
|
||||||
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
|
PendingIntent replyPendingIntent = PendingIntent.getBroadcast(
|
||||||
mContext,
|
mContext,
|
||||||
|
|
@ -540,8 +552,8 @@ public class CustomPushNotification extends PushNotification {
|
||||||
return message.replaceFirst(": ", "").trim();
|
return message.replaceFirst(": ", "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notificationReceiptDelivery(String ackId, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
|
||||||
ReceiptDelivery.send(context, ackId, postId, type, isIdLoaded, promise);
|
ReceiptDelivery.send(context, ackId, serverUrl, postId, type, isIdLoaded, promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createNotificationChannels() {
|
private void createNotificationChannels() {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ import com.facebook.soloader.SoLoader;
|
||||||
|
|
||||||
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
||||||
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
||||||
import org.unimodules.core.interfaces.SingletonModule;
|
|
||||||
|
|
||||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,11 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||||
final int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
|
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
|
@Override
|
||||||
public void resolve(@Nullable Object value) {
|
public void resolve(@Nullable Object value) {
|
||||||
if (value instanceof Boolean && !(Boolean)value) {
|
if (value instanceof Boolean && !(Boolean)value) {
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,12 @@ import com.facebook.react.bridge.WritableMap;
|
||||||
import com.mattermost.helpers.*;
|
import com.mattermost.helpers.*;
|
||||||
|
|
||||||
public class ReceiptDelivery {
|
public class ReceiptDelivery {
|
||||||
static final String CURRENT_SERVER_URL = "@currentServerUrl";
|
|
||||||
|
|
||||||
private static final int[] FIBONACCI_BACKOFFS = new int[] { 0, 1, 2, 3, 5, 8 };
|
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);
|
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||||
|
|
||||||
Credentials.getCredentialsForCurrentServer(reactApplicationContext, new ResolvePromise() {
|
Credentials.getCredentialsForServer(reactApplicationContext, serverUrl, new ResolvePromise() {
|
||||||
@Override
|
@Override
|
||||||
public void resolve(@Nullable Object value) {
|
public void resolve(@Nullable Object value) {
|
||||||
if (value instanceof Boolean && !(Boolean)value) {
|
if (value instanceof Boolean && !(Boolean)value) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
<item name="android:windowBackground">@android:color/transparent</item>
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="LightTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
<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.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import Attachment from './attachment';
|
import Attachment from './attachment';
|
||||||
|
import SSO from './sso';
|
||||||
import Database from './database';
|
import Database from './database';
|
||||||
import DeepLink from './deep_linking';
|
|
||||||
import Device from './device';
|
import Device from './device';
|
||||||
|
import Files from './files';
|
||||||
|
import General from './general';
|
||||||
import List from './list';
|
import List from './list';
|
||||||
import Navigation from './navigation';
|
import Navigation from './navigation';
|
||||||
import Preferences from './preferences';
|
import Preferences from './preferences';
|
||||||
|
|
@ -14,9 +16,11 @@ import WebsocketEvents from './websocket';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Attachment,
|
Attachment,
|
||||||
|
SSO,
|
||||||
Database,
|
Database,
|
||||||
DeepLink,
|
|
||||||
Device,
|
Device,
|
||||||
|
Files,
|
||||||
|
General,
|
||||||
List,
|
List,
|
||||||
Navigation,
|
Navigation,
|
||||||
Preferences,
|
Preferences,
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,23 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
export const CHANNEL = 'Channel';
|
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 MAIN_SIDEBAR = 'MainSidebar';
|
||||||
|
export const MFA = 'MFA';
|
||||||
export const SERVER = 'Server';
|
export const SERVER = 'Server';
|
||||||
export const SETTINGS_SIDEBAR = 'SettingsSidebar';
|
export const SETTINGS_SIDEBAR = 'SettingsSidebar';
|
||||||
|
export const SSO = 'SSO';
|
||||||
export const THREAD = 'Thread';
|
export const THREAD = 'Thread';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
CHANNEL,
|
CHANNEL,
|
||||||
|
FORGOT_PASSWORD,
|
||||||
|
LOGIN,
|
||||||
|
LOGIN_OPTIONS,
|
||||||
MAIN_SIDEBAR,
|
MAIN_SIDEBAR,
|
||||||
|
MFA,
|
||||||
SERVER,
|
SERVER,
|
||||||
SETTINGS_SIDEBAR,
|
SETTINGS_SIDEBAR,
|
||||||
THREAD,
|
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,
|
REMOVE_LAST_CHANNEL_FOR_TEAM: null,
|
||||||
|
|
||||||
GITLAB: null,
|
|
||||||
OFFICE365: null,
|
|
||||||
SAML: null,
|
|
||||||
|
|
||||||
SET_INITIAL_POST_VISIBILITY: null,
|
SET_INITIAL_POST_VISIBILITY: null,
|
||||||
RECEIVED_FOCUSED_POST: null,
|
RECEIVED_FOCUSED_POST: null,
|
||||||
LOADING_POSTS: null,
|
LOADING_POSTS: null,
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,23 @@ import {
|
||||||
} from '@database/models/server';
|
} from '@database/models/server';
|
||||||
import {serverSchema} from '@database/schema/server';
|
import {serverSchema} from '@database/schema/server';
|
||||||
import logger from '@nozbe/watermelondb/utils/common/logger';
|
import logger from '@nozbe/watermelondb/utils/common/logger';
|
||||||
import type {
|
import {
|
||||||
ActiveServerDatabaseArgs,
|
|
||||||
DatabaseConnectionArgs,
|
DatabaseConnectionArgs,
|
||||||
DatabaseInstance,
|
DatabaseInstance,
|
||||||
DatabaseInstances,
|
DatabaseInstances,
|
||||||
DefaultNewServerArgs,
|
DefaultNewServerArgs,
|
||||||
|
GetDatabaseConnectionArgs,
|
||||||
Models,
|
Models,
|
||||||
|
RetrievedDatabase,
|
||||||
} from '@typings/database/database';
|
} from '@typings/database/database';
|
||||||
import {DatabaseType} from '@typings/database/enums';
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
import IGlobal from '@typings/database/global';
|
||||||
import IServers from '@typings/database/servers';
|
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__) {
|
if (__DEV__) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|
@ -61,7 +66,6 @@ if (__DEV__) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DatabaseManager {
|
class DatabaseManager {
|
||||||
private activeDatabase: DatabaseInstance;
|
|
||||||
private defaultDatabase: DatabaseInstance;
|
private defaultDatabase: DatabaseInstance;
|
||||||
private readonly defaultModels: Models;
|
private readonly defaultModels: Models;
|
||||||
private readonly iOSAppGroupDatabase: string | null;
|
private readonly iOSAppGroupDatabase: string | null;
|
||||||
|
|
@ -100,11 +104,58 @@ class DatabaseManager {
|
||||||
TermsOfService,
|
TermsOfService,
|
||||||
User,
|
User,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.iOSAppGroupDatabase = null;
|
this.iOSAppGroupDatabase = null;
|
||||||
this.androidFilesDirectory = 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,
|
* 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.
|
* if a database connection could not be created, it will return undefined.
|
||||||
|
|
@ -114,28 +165,17 @@ class DatabaseManager {
|
||||||
* @returns {Promise<DatabaseInstance>}
|
* @returns {Promise<DatabaseInstance>}
|
||||||
*/
|
*/
|
||||||
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
||||||
const {
|
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
|
||||||
actionsEnabled = true,
|
|
||||||
dbName = 'default',
|
|
||||||
dbType = DatabaseType.DEFAULT,
|
|
||||||
serverUrl = undefined,
|
|
||||||
} = configs;
|
|
||||||
|
|
||||||
try {
|
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 migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
|
||||||
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
||||||
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
|
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
|
||||||
|
|
||||||
const adapter = new LokiJSAdapter({
|
const adapter = new LokiJSAdapter({dbName: databaseName, migrations, schema, useWebWorker: false, useIncrementalIndexedDB: true});
|
||||||
dbName: databaseName,
|
|
||||||
migrations,
|
|
||||||
schema,
|
|
||||||
useWebWorker: false,
|
|
||||||
useIncrementalIndexedDB: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Registers the new server connection into the DEFAULT database
|
// Registers the new server connection into the DEFAULT database
|
||||||
if (serverUrl && shouldAddToDefaultDatabase) {
|
if (serverUrl && shouldAddToDefaultDatabase) {
|
||||||
|
|
@ -147,7 +187,7 @@ class DatabaseManager {
|
||||||
}
|
}
|
||||||
return new Database({adapter, actionsEnabled, modelClasses});
|
return new Database({adapter, actionsEnabled, modelClasses});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('createDatabaseConnection ERROR:', e);
|
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.
|
* 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.
|
* This method should be called when switching to another server.
|
||||||
* @param {string} displayName
|
|
||||||
* @param {string} serverUrl
|
* @param {string} serverUrl
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
setActiveServerDatabase = async ({displayName, serverUrl}: ActiveServerDatabaseArgs) => {
|
setActiveServerDatabase = async (serverUrl: string) => {
|
||||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
const defaultDatabase = await this.getDefaultDatabase();
|
||||||
|
|
||||||
this.activeDatabase = await this.createDatabaseConnection({
|
if (defaultDatabase) {
|
||||||
configs: {
|
// retrieve recentlyViewedServers from Global entity
|
||||||
actionsEnabled: true,
|
const recentlyViewedServers = (await defaultDatabase.collections.get(GLOBAL).query(Q.where('name', RECENTLY_VIEWED_SERVERS)).fetch()) as IGlobal[];
|
||||||
dbName: displayName,
|
|
||||||
dbType: DatabaseType.SERVER,
|
if (recentlyViewedServers.length) {
|
||||||
serverUrl,
|
// We have previously written something for this flag
|
||||||
},
|
const flagRecord = recentlyViewedServers[0];
|
||||||
shouldAddToDefaultDatabase: Boolean(!isServerPresent),
|
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>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
isServerPresent = async (serverUrl: string) => {
|
isServerPresent = async (serverUrl: string) => {
|
||||||
const allServers = await this.getAllServers();
|
const allServers = await this.getAllServers([serverUrl]);
|
||||||
const existingServer = allServers?.filter((server) => {
|
return allServers?.length > 0;
|
||||||
return server.url === serverUrl;
|
|
||||||
});
|
|
||||||
return existingServer && existingServer.length > 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getActiveServerDatabase: The DatabaseManager should be the only one setting the active database. Hence, we have made the activeDatabase property private.
|
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
|
||||||
* Use this getter method to retrieve the active database if it has been set in your code.
|
* @returns {string}
|
||||||
* @returns {DatabaseInstance}
|
|
||||||
*/
|
*/
|
||||||
getActiveServerDatabase = (): DatabaseInstance => {
|
getActiveServerUrl = async (): Promise<string|undefined> => {
|
||||||
return this.activeDatabase;
|
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.
|
* getDefaultDatabase : Returns the default database.
|
||||||
|
|
@ -213,41 +293,31 @@ class DatabaseManager {
|
||||||
* and return them to the caller.
|
* and return them to the caller.
|
||||||
*
|
*
|
||||||
* @param {string[]} serverUrls
|
* @param {string[]} serverUrls
|
||||||
* @returns {Promise<DatabaseInstances[] | null>}
|
* @returns {Promise<RetrievedDatabase[] | null>}
|
||||||
*/
|
*/
|
||||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<DatabaseInstances[] | null> => {
|
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
|
||||||
if (serverUrls?.length) {
|
// Retrieve all server records from the default db
|
||||||
// Retrieve all server records from the default db
|
const allServers = await this.getAllServers(serverUrls);
|
||||||
const allServers = await this.getAllServers();
|
|
||||||
|
|
||||||
// Filter only those servers that are present in the serverUrls array
|
// Creates server database instances
|
||||||
const servers = allServers!.filter((server: IServers) => {
|
if (allServers.length) {
|
||||||
return serverUrls.includes(server.url);
|
const databasePromises = allServers.map(async (server: IServers) => {
|
||||||
});
|
const {displayName, url} = server;
|
||||||
|
|
||||||
// Creates server database instances
|
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||||
if (servers.length) {
|
const dbInstance = await this.createDatabaseConnection({
|
||||||
const databasePromises = servers.map(async (server: IServers) => {
|
configs: {
|
||||||
const {displayName, url} = server;
|
actionsEnabled: true,
|
||||||
|
dbName: displayName,
|
||||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
dbType: DatabaseType.SERVER,
|
||||||
const dbInstance = await this.createDatabaseConnection({
|
serverUrl: url,
|
||||||
configs: {
|
},
|
||||||
actionsEnabled: true,
|
shouldAddToDefaultDatabase: false,
|
||||||
dbName: displayName,
|
|
||||||
dbType: DatabaseType.SERVER,
|
|
||||||
serverUrl: url,
|
|
||||||
},
|
|
||||||
shouldAddToDefaultDatabase: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {url, dbInstance};
|
|
||||||
});
|
});
|
||||||
|
return {url, dbInstance, displayName};
|
||||||
const databaseInstances = await Promise.all(databasePromises);
|
});
|
||||||
return databaseInstances;
|
const databaseInstances = await Promise.all(databasePromises);
|
||||||
}
|
return databaseInstances;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
@ -260,26 +330,40 @@ class DatabaseManager {
|
||||||
*/
|
*/
|
||||||
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const defaultDB = await this.getDefaultDatabase();
|
const defaultDatabase = await this.getDefaultDatabase();
|
||||||
let server: IServers;
|
let server: IServers;
|
||||||
|
let result = true;
|
||||||
if (defaultDB) {
|
if (defaultDatabase) {
|
||||||
const serversRecords = (await defaultDB.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
||||||
server = serversRecords?.[0] ?? undefined;
|
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) {
|
if (server) {
|
||||||
// Perform a delete operation for this server record on the 'servers' table in default database
|
// 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();
|
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;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log('An error occurred while trying to delete database with name ', databaseName);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -288,11 +372,17 @@ class DatabaseManager {
|
||||||
* getAllServers : Retrieves all the servers registered in the default database
|
* getAllServers : Retrieves all the servers registered in the default database
|
||||||
* @returns {Promise<undefined | Servers[]>}
|
* @returns {Promise<undefined | Servers[]>}
|
||||||
*/
|
*/
|
||||||
private getAllServers = async () => {
|
private getAllServers = async (serverUrls: string[]) => {
|
||||||
// Retrieve all server records from the default db
|
// Retrieve all server records from the default db
|
||||||
const defaultDatabase = await this.getDefaultDatabase();
|
const defaultDatabase = await this.getDefaultDatabase();
|
||||||
const allServers = defaultDatabase && ((await defaultDatabase.collections.get(MM_TABLES.DEFAULT.SERVERS).query().fetch()) as IServers[]);
|
if (defaultDatabase) {
|
||||||
return allServers;
|
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> => {
|
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||||
this.defaultDatabase = await this.createDatabaseConnection({
|
this.defaultDatabase = await this.createDatabaseConnection({
|
||||||
configs: {dbName: 'default'},
|
configs: {dbName: DEFAULT_DATABASE},
|
||||||
shouldAddToDefaultDatabase: false,
|
shouldAddToDefaultDatabase: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -322,7 +412,7 @@ class DatabaseManager {
|
||||||
|
|
||||||
if (defaultDatabase && !isServerPresent) {
|
if (defaultDatabase && !isServerPresent) {
|
||||||
await defaultDatabase.action(async () => {
|
await defaultDatabase.action(async () => {
|
||||||
const serversCollection = defaultDatabase.collections.get('servers');
|
const serversCollection = defaultDatabase.collections.get(SERVERS);
|
||||||
await serversCollection.create((server: IServers) => {
|
await serversCollection.create((server: IServers) => {
|
||||||
server.dbPath = databaseFilePath;
|
server.dbPath = databaseFilePath;
|
||||||
server.displayName = displayName;
|
server.displayName = displayName;
|
||||||
|
|
@ -333,9 +423,10 @@ class DatabaseManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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,
|
User,
|
||||||
} from '@database/models/server';
|
} from '@database/models/server';
|
||||||
import {serverSchema} from '@database/schema/server';
|
import {serverSchema} from '@database/schema/server';
|
||||||
import type {
|
import {
|
||||||
ActiveServerDatabaseArgs,
|
|
||||||
DatabaseConnectionArgs,
|
DatabaseConnectionArgs,
|
||||||
DatabaseInstance,
|
DatabaseInstance,
|
||||||
DatabaseInstances,
|
DatabaseInstances,
|
||||||
DefaultNewServerArgs,
|
DefaultNewServerArgs,
|
||||||
|
GetDatabaseConnectionArgs,
|
||||||
MigrationEvents,
|
MigrationEvents,
|
||||||
Models,
|
Models,
|
||||||
|
RetrievedDatabase,
|
||||||
} from '@typings/database/database';
|
} from '@typings/database/database';
|
||||||
import {DatabaseType} from '@typings/database/enums';
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
import IServers from '@typings/database/servers';
|
import IServers from '@typings/database/servers';
|
||||||
|
import IGlobal from '@typings/database/global';
|
||||||
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
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 {
|
class DatabaseManager {
|
||||||
private activeDatabase: DatabaseInstance;
|
|
||||||
private defaultDatabase: DatabaseInstance;
|
private defaultDatabase: DatabaseInstance;
|
||||||
private readonly defaultModels: Models;
|
private readonly defaultModels: Models;
|
||||||
private readonly iOSAppGroupDatabase: string | null;
|
private readonly iOSAppGroupDatabase: string | null;
|
||||||
|
|
@ -103,6 +107,50 @@ class DatabaseManager {
|
||||||
this.androidFilesDirectory = Platform.OS === 'android' ? FileSystem.documentDirectory : null;
|
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,
|
* 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.
|
* if a database connection could not be created, it will return undefined.
|
||||||
|
|
@ -112,16 +160,12 @@ class DatabaseManager {
|
||||||
* @returns {Promise<DatabaseInstance>}
|
* @returns {Promise<DatabaseInstance>}
|
||||||
*/
|
*/
|
||||||
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
createDatabaseConnection = async ({configs, shouldAddToDefaultDatabase = true}: DatabaseConnectionArgs): Promise<DatabaseInstance> => {
|
||||||
const {
|
const {actionsEnabled = true, dbName = DEFAULT_DATABASE, dbType = DatabaseType.DEFAULT, serverUrl = undefined} = configs;
|
||||||
actionsEnabled = true,
|
|
||||||
dbName = 'default',
|
|
||||||
dbType = DatabaseType.DEFAULT,
|
|
||||||
serverUrl = undefined,
|
|
||||||
} = configs;
|
|
||||||
|
|
||||||
try {
|
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 migrations = dbType === DatabaseType.DEFAULT ? DefaultMigration : ServerMigration;
|
||||||
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
const modelClasses = dbType === DatabaseType.DEFAULT ? this.defaultModels : this.serverModels;
|
||||||
const schema = dbType === DatabaseType.DEFAULT ? defaultSchema : serverSchema;
|
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.
|
* 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.
|
* This method should be called when switching to another server.
|
||||||
* @param {string} displayName
|
|
||||||
* @param {string} serverUrl
|
* @param {string} serverUrl
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
setActiveServerDatabase = async ({displayName, serverUrl}: ActiveServerDatabaseArgs) => {
|
setActiveServerDatabase = async (serverUrl: string) => {
|
||||||
const isServerPresent = await this.isServerPresent(serverUrl);
|
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({
|
if (recentlyViewedServers.length) {
|
||||||
configs: {
|
// We have previously written something for this flag
|
||||||
actionsEnabled: true,
|
const flagRecord = recentlyViewedServers[0];
|
||||||
dbName: displayName,
|
const recentList = Array.from(flagRecord.value);
|
||||||
dbType: DatabaseType.SERVER,
|
recentList.unshift(serverUrl);
|
||||||
serverUrl,
|
|
||||||
},
|
// so as to avoid duplicate in this array
|
||||||
shouldAddToDefaultDatabase: Boolean(!isServerPresent),
|
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
|
* getActiveServerUrl: Use this getter method to retrieve the active server URL.
|
||||||
* @param {String} serverUrl
|
* @returns {string | undefined}
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
*/
|
||||||
private isServerPresent = async (serverUrl: string) => {
|
getActiveServerUrl = async (): Promise<string | undefined> => {
|
||||||
const allServers = await this.getAllServers();
|
const defaultDatabase = await this.getDefaultDatabase();
|
||||||
|
|
||||||
const existingServer = allServers?.filter((server) => {
|
if (defaultDatabase) {
|
||||||
return server.url === serverUrl;
|
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.
|
* 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.
|
* 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 => {
|
getActiveServerDatabase = async (): Promise<DatabaseInstance> => {
|
||||||
return this.activeDatabase;
|
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.
|
* and return them to the caller.
|
||||||
*
|
*
|
||||||
* @param {string[]} serverUrls
|
* @param {string[]} serverUrls
|
||||||
* @returns {Promise<{url: string, dbInstance: DatabaseInstance}[] | null>}
|
* @returns {Promise<RetrievedDatabase[] | null>}
|
||||||
*/
|
*/
|
||||||
retrieveDatabaseInstances = async (serverUrls?: string[]): Promise<DatabaseInstances[] | null> => {
|
retrieveDatabaseInstances = async (serverUrls: string[]): Promise<DatabaseInstances[] | null> => {
|
||||||
if (serverUrls?.length) {
|
// Retrieve all server records from the default db
|
||||||
// Retrieve all server records from the default db
|
const allServers = await this.getAllServers(serverUrls);
|
||||||
const allServers = await this.getAllServers();
|
|
||||||
|
|
||||||
// Filter only those servers that are present in the serverUrls array
|
// Creates server database instances
|
||||||
const servers = allServers!.filter((server: IServers) => {
|
if (allServers.length) {
|
||||||
return serverUrls.includes(server.url);
|
const databasePromises = await allServers.map(async (server: IServers) => {
|
||||||
});
|
const {displayName, url} = server;
|
||||||
|
|
||||||
// Creates server database instances
|
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
||||||
if (servers.length) {
|
const dbInstance = await this.createDatabaseConnection({
|
||||||
const databasePromises = await servers.map(async (server: IServers) => {
|
configs: {
|
||||||
const {displayName, url} = server;
|
actionsEnabled: true,
|
||||||
|
dbName: displayName,
|
||||||
// Since we are retrieving existing URL ( and so database connections ) from the 'DEFAULT' database, shouldAddToDefaultDatabase is set to false
|
dbType: DatabaseType.SERVER,
|
||||||
const dbInstance = await this.createDatabaseConnection({
|
serverUrl: url,
|
||||||
configs: {
|
},
|
||||||
actionsEnabled: true,
|
shouldAddToDefaultDatabase: false,
|
||||||
dbName: displayName,
|
|
||||||
dbType: DatabaseType.SERVER,
|
|
||||||
serverUrl: url,
|
|
||||||
},
|
|
||||||
shouldAddToDefaultDatabase: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {url, dbInstance};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const databaseInstances = await Promise.all(databasePromises);
|
return {dbInstance, displayName, url};
|
||||||
return databaseInstances;
|
});
|
||||||
}
|
const databaseInstances = await Promise.all(databasePromises);
|
||||||
return null;
|
return databaseInstances;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
@ -258,24 +320,26 @@ class DatabaseManager {
|
||||||
*/
|
*/
|
||||||
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
deleteDatabase = async (serverUrl: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
const defaultDB = await this.getDefaultDatabase();
|
const defaultDatabase = await this.getDefaultDatabase();
|
||||||
let server: IServers;
|
let server: IServers;
|
||||||
|
let result = true;
|
||||||
|
|
||||||
if (defaultDB) {
|
if (defaultDatabase) {
|
||||||
const serversRecords = (await defaultDB.collections.
|
// NOTE: We are deleting this 'database' entry in the SERVERS entity on the DEFAULT database; for that we retrieve its record first.
|
||||||
get(SERVERS).
|
const serversRecords = (await defaultDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as IServers[];
|
||||||
query(Q.where('url', serverUrl)).
|
|
||||||
fetch()) as IServers[];
|
|
||||||
server = serversRecords?.[0] ?? undefined;
|
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) {
|
if (server) {
|
||||||
|
const databaseName = server.displayName;
|
||||||
|
|
||||||
// Perform a delete operation for this server record on the 'servers' table in default database
|
// 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();
|
await server.destroyPermanently();
|
||||||
});
|
});
|
||||||
|
|
||||||
const databaseName = server.displayName;
|
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
|
// On iOS, we'll delete the *.db file under the shared app-group/databases folder
|
||||||
deleteIOSDatabase({databaseName});
|
deleteIOSDatabase({databaseName});
|
||||||
|
|
@ -290,13 +354,24 @@ class DatabaseManager {
|
||||||
await FileSystem.deleteAsync(databaseFile);
|
await FileSystem.deleteAsync(databaseFile);
|
||||||
await FileSystem.deleteAsync(databaseJournal);
|
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;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log('An error occurred while trying to delete database with name ', databaseName);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -308,7 +383,7 @@ class DatabaseManager {
|
||||||
*/
|
*/
|
||||||
factoryReset = async (shouldRemoveDirectory: boolean): Promise<boolean> => {
|
factoryReset = async (shouldRemoveDirectory: boolean): Promise<boolean> => {
|
||||||
try {
|
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') {
|
if (Platform.OS === 'ios') {
|
||||||
deleteIOSDatabase({shouldRemoveDirectory});
|
deleteIOSDatabase({shouldRemoveDirectory});
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -319,20 +394,34 @@ class DatabaseManager {
|
||||||
await FileSystem.deleteAsync(androidFilesDir);
|
await FileSystem.deleteAsync(androidFilesDir);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.log('An error occurred while trying to delete the databases directory);
|
|
||||||
return false;
|
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
|
* 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
|
// Retrieve all server records from the default db
|
||||||
const defaultDatabase = await this.getDefaultDatabase();
|
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> => {
|
private setDefaultDatabase = async (): Promise<DatabaseInstance> => {
|
||||||
this.defaultDatabase = await this.createDatabaseConnection({
|
this.defaultDatabase = await this.createDatabaseConnection({
|
||||||
configs: {dbName: 'default'},
|
configs: {dbName: DEFAULT_DATABASE},
|
||||||
shouldAddToDefaultDatabase: false,
|
shouldAddToDefaultDatabase: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -362,7 +451,7 @@ class DatabaseManager {
|
||||||
|
|
||||||
if (defaultDatabase && !isServerPresent) {
|
if (defaultDatabase && !isServerPresent) {
|
||||||
await defaultDatabase.action(async () => {
|
await defaultDatabase.action(async () => {
|
||||||
const serversCollection = defaultDatabase.collections.get('servers');
|
const serversCollection = defaultDatabase.collections.get(SERVERS);
|
||||||
await serversCollection.create((server: IServers) => {
|
await serversCollection.create((server: IServers) => {
|
||||||
server.dbPath = databaseFilePath;
|
server.dbPath = databaseFilePath;
|
||||||
server.displayName = displayName;
|
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.
|
* 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.
|
* 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"}
|
* 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
|
* @param {string} dbName
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
private getDatabaseDirectory = (dbName: string): string => {
|
private getDatabaseFilePath = (dbName: string): string => {
|
||||||
return Platform.OS === 'ios' ? `${this.iOSAppGroupDatabase}/${dbName}.db` : `${FileSystem.documentDirectory}${dbName}.db`;
|
return Platform.OS === 'ios' ? `${this.iOSAppGroupDatabase}/${dbName}.db` : `${FileSystem.documentDirectory}${dbName}.db`;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -428,4 +517,4 @@ if (!__DEV__) {
|
||||||
logger.silence();
|
logger.silence();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseManager();
|
export default DatabaseManager;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {Database} from '@nozbe/watermelondb';
|
import {Database, Q} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
import {DatabaseInstance} from '@typings/database/database';
|
|
||||||
import {DatabaseType} from '@typings/database/enums';
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
import IGlobal from '@typings/database/global';
|
||||||
import IServers from '@typings/database/servers';
|
import IServers from '@typings/database/servers';
|
||||||
|
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
|
|
||||||
jest.mock('@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 :
|
// NOTE : On the mock Database Manager, we cannot test for :
|
||||||
// 1. Android/iOS file path
|
// 1. Android/iOS file path
|
||||||
|
|
@ -21,12 +22,43 @@ const {SERVERS} = MM_TABLES.DEFAULT;
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Database Manager tests ***', () => {
|
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 () => {
|
it('=> should return a default database', async () => {
|
||||||
expect.assertions(2);
|
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(defaultDB).toBeInstanceOf(Database);
|
||||||
expect(spyOnAddServerToDefaultDatabase).not.toHaveBeenCalledTimes(1);
|
expect(spyOnAddServerToDefaultDatabase).not.toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -35,9 +67,9 @@ describe('*** Database Manager tests ***', () => {
|
||||||
it('=> should create a new server connection', async () => {
|
it('=> should create a new server connection', async () => {
|
||||||
expect.assertions(2);
|
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,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
@ -52,53 +84,59 @@ describe('*** Database Manager tests ***', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('=> should switch between active server connections', async () => {
|
it('=> should switch between active server connections', async () => {
|
||||||
expect.assertions(5);
|
expect.assertions(6);
|
||||||
let activeServer: DatabaseInstance;
|
|
||||||
let adapter;
|
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
|
// 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
|
// 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
|
// let's verify if we now have a value for activeServer
|
||||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
const activeServerB = await databaseManagerClient!.getActiveServerDatabase();
|
||||||
expect(activeServer).toBeDefined();
|
expect(activeServerB).toBeDefined();
|
||||||
|
|
||||||
adapter = activeServer!.adapter as any;
|
adapter = activeServerB!.adapter as any;
|
||||||
const currentDBName = adapter.underlyingAdapter._dbName;
|
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
|
// 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'});
|
await setActiveServer('https://appv2.mattermost.com');
|
||||||
activeServer = await DatabaseManager.getActiveServerDatabase();
|
const activeServerC = await databaseManagerClient!.getActiveServerDatabase();
|
||||||
expect(activeServer).toBeDefined();
|
expect(activeServerC).toBeDefined();
|
||||||
|
|
||||||
adapter = activeServer!.adapter as any;
|
adapter = activeServerC!.adapter as any;
|
||||||
const newDBName = adapter.underlyingAdapter._dbName;
|
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 () => {
|
it('=> should retrieve all database instances matching serverUrls parameter', async () => {
|
||||||
expect.assertions(3);
|
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://xunity2.mattermost.com',
|
||||||
'https://appv2.mattermost.com',
|
'https://appv2.mattermost.com',
|
||||||
'https://appv1.mattermost.com',
|
'https://appv1.mattermost.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(dbInstances).toBeTruthy();
|
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
|
// The Database Manager will call the 'createDatabaseConnection' method in consequence of the number of database connection present in dbInstances array
|
||||||
expect(spyOnCreateDatabaseConnection).toHaveBeenCalledTimes(numDbInstances);
|
expect(spyOnCreateDatabaseConnection).toHaveBeenCalledTimes(numDbInstances);
|
||||||
|
|
@ -107,12 +145,23 @@ describe('*** Database Manager tests ***', () => {
|
||||||
expect(numDbInstances).toEqual(2);
|
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 () => {
|
it('=> should have records of Servers set in the servers table of the default database', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
const defaultDB = await databaseManagerClient!.getDefaultDatabase();
|
||||||
expect(defaultDB).toBeDefined();
|
expect(defaultDB).toBeDefined();
|
||||||
|
await createTwoConnections();
|
||||||
const serversRecords = await defaultDB!.collections.get(SERVERS).query().fetch() as IServers[];
|
const serversRecords = await defaultDB!.collections.get(SERVERS).query().fetch() as IServers[];
|
||||||
expect(serversRecords).toBeDefined();
|
expect(serversRecords).toBeDefined();
|
||||||
|
|
||||||
|
|
@ -122,16 +171,29 @@ describe('*** Database Manager tests ***', () => {
|
||||||
|
|
||||||
it('=> should delete appv1 server from the servers table of Default database', async () => {
|
it('=> should delete appv1 server from the servers table of Default database', async () => {
|
||||||
expect.assertions(3);
|
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
|
// 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);
|
expect(isAppV1Removed).toBe(true);
|
||||||
|
|
||||||
// Verifying in the database to confirm if its record was deleted
|
// 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[];
|
const updatedRecentServers = await fetchGlobalRecords();
|
||||||
expect(serversRecords).toBeDefined();
|
expect(updatedRecentServers.length).toBe(1);
|
||||||
expect(serversRecords.length).toEqual(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('=> should enforce uniqueness of connections using serverUrl as key', async () => {
|
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
|
// We can't have more than one connection with the same server url
|
||||||
const serverUrl = 'https://appv3.mattermost.com';
|
const serverUrl = 'https://appv3.mattermost.com';
|
||||||
await DatabaseManager.createDatabaseConnection({
|
await databaseManagerClient!.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
@ -149,7 +211,7 @@ describe('*** Database Manager tests ***', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await DatabaseManager.createDatabaseConnection({
|
await databaseManagerClient!.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
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[];
|
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 {DatabaseType} from '@typings/database/enums';
|
||||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||||
|
|
||||||
import DBManager from './index';
|
import DatabaseManager from './index';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
const databaseClient = new DatabaseManager();
|
||||||
|
|
||||||
// Test: It should return the iOS App-Group shared directory
|
// Test: It should return the iOS App-Group shared directory
|
||||||
const testAppGroupDirectory = () => {
|
const testAppGroupDirectory = () => {
|
||||||
if (Platform.OS === 'ios') {
|
if (Platform.OS === 'ios') {
|
||||||
|
|
@ -18,12 +20,12 @@ export default async () => {
|
||||||
|
|
||||||
// Test: It should return an instance of the default database
|
// Test: It should return an instance of the default database
|
||||||
const testGetDefaultDatabase = () => {
|
const testGetDefaultDatabase = () => {
|
||||||
DBManager.getDefaultDatabase();
|
databaseClient.getDefaultDatabase();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test: It should creates a new server connection
|
// Test: It should creates a new server connection
|
||||||
const testNewServerConnection = async () => {
|
const testNewServerConnection = async () => {
|
||||||
await DBManager.createDatabaseConnection({
|
await databaseClient.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
@ -36,20 +38,17 @@ export default async () => {
|
||||||
|
|
||||||
// Test: It should return the current active server database
|
// Test: It should return the current active server database
|
||||||
const testGetActiveServerConnection = () => {
|
const testGetActiveServerConnection = () => {
|
||||||
// const activeServer = DBManager.getActiveServerDatabase();
|
// const activeServer = DatabaseManager.getActiveServerDatabase();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test: It should set the current active server database to the provided server url.
|
// Test: It should set the current active server database to the provided server url.
|
||||||
const testSetActiveServerConnection = async () => {
|
const testSetActiveServerConnection = async () => {
|
||||||
await DBManager.setActiveServerDatabase({
|
await databaseClient.setActiveServerDatabase('https://comm4.mattermost.com');
|
||||||
displayName: 'comm4',
|
|
||||||
serverUrl: 'https://comm4.mattermost.com',
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test: It should return database instance(s) if there are valid server urls in the provided list.
|
// Test: It should return database instance(s) if there are valid server urls in the provided list.
|
||||||
const testRetrieveAllDatabaseConnections = async () => {
|
const testRetrieveAllDatabaseConnections = async () => {
|
||||||
await DBManager.retrieveDatabaseInstances([
|
await databaseClient.retrieveDatabaseInstances([
|
||||||
'https://xunity2.mattermost.com',
|
'https://xunity2.mattermost.com',
|
||||||
'https://comm5.mattermost.com',
|
'https://comm5.mattermost.com',
|
||||||
'https://comm4.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
|
// Test: It should delete the associated *.db file for this server url
|
||||||
const testDeleteSQLFile = async () => {
|
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
|
// 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 () => {
|
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.
|
// 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;
|
@field('name') name!: string;
|
||||||
|
|
||||||
/** value : The value part of the key-value combination */
|
/** 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) => {
|
export const isRecordSystemEqualToRaw = (record: System, raw: RawSystem) => {
|
||||||
return raw.id === record.id;
|
return raw.name === record.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isRecordTermsOfServiceEqualToRaw = (record: TermsOfService, raw: RawTermsOfService) => {
|
export const isRecordTermsOfServiceEqualToRaw = (record: TermsOfService, raw: RawTermsOfService) => {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {DataOperator} from '@database/operator';
|
import Operator from '@database/operator';
|
||||||
import {
|
import {
|
||||||
isRecordAppEqualToRaw,
|
isRecordAppEqualToRaw,
|
||||||
isRecordCustomEmojiEqualToRaw,
|
isRecordCustomEmojiEqualToRaw,
|
||||||
|
|
@ -24,22 +24,40 @@ import {
|
||||||
} from '@database/operator/prepareRecords/general';
|
} from '@database/operator/prepareRecords/general';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
import {RawGlobal, RawRole, RawServers, RawTermsOfService} from '@typings/database/database';
|
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');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** DataOperator: Base Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleApp: should write to APP entity', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(defaultDB).toBeTruthy();
|
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,
|
tableName: IsolatedEntities.APP,
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
|
@ -81,13 +99,13 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
|
it('=> HandleGlobal: should write to GLOBAL entity', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(defaultDB).toBeTruthy();
|
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'}];
|
const values: RawGlobal[] = [{name: 'global-1-name', value: 'global-1-value'}];
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.GLOBAL,
|
tableName: IsolatedEntities.GLOBAL,
|
||||||
values,
|
values,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
|
|
@ -106,10 +124,10 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
it('=> HandleServers: should write to SERVERS entity', async () => {
|
it('=> HandleServers: should write to SERVERS entity', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(defaultDB).toBeTruthy();
|
expect(defaultDatabase).toBeTruthy();
|
||||||
|
|
||||||
const spyOnHandleEntityRecords = jest.spyOn(DataOperator as any, 'handleEntityRecords');
|
const spyOnHandleEntityRecords = jest.spyOn(operatorClient as any, 'handleEntityRecords');
|
||||||
|
|
||||||
const values: RawServers[] = [
|
const values: RawServers[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -121,7 +139,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.SERVERS,
|
tableName: IsolatedEntities.SERVERS,
|
||||||
values,
|
values,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
|
|
@ -150,7 +168,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
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[] = [
|
const values: RawRole[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -160,7 +178,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.ROLE,
|
tableName: IsolatedEntities.ROLE,
|
||||||
values,
|
values,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
|
|
@ -185,11 +203,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => {
|
it('=> HandleCustomEmojis: should write to CUSTOM_EMOJI entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'base_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
tableName: IsolatedEntities.CUSTOM_EMOJI,
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
|
|
@ -229,11 +247,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
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'}];
|
const values = [{id: 'system-id-1', name: 'system-1', value: 'system-1'}];
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.SYSTEM,
|
tableName: IsolatedEntities.SYSTEM,
|
||||||
values,
|
values,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
|
|
@ -241,7 +259,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
|
|
||||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||||
findMatchingRecordBy: isRecordSystemEqualToRaw,
|
findMatchingRecordBy: isRecordSystemEqualToRaw,
|
||||||
fieldName: 'id',
|
fieldName: 'name',
|
||||||
operator: prepareSystemRecord,
|
operator: prepareSystemRecord,
|
||||||
rawValues: values,
|
rawValues: values,
|
||||||
tableName: 'System',
|
tableName: 'System',
|
||||||
|
|
@ -254,7 +272,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'base_handler', setActive: true});
|
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[] = [
|
const values: RawTermsOfService[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -266,7 +284,7 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await DataOperator.handleIsolatedEntity({
|
await operatorClient.handleIsolatedEntity({
|
||||||
tableName: IsolatedEntities.TERMS_OF_SERVICE,
|
tableName: IsolatedEntities.TERMS_OF_SERVICE,
|
||||||
values,
|
values,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
|
|
@ -285,11 +303,11 @@ describe('*** DataOperator: Base Handlers tests ***', () => {
|
||||||
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
|
it('=> No table name: should not call executeInDatabase if tableName is invalid', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
const defaultDB = await DatabaseManager.getDefaultDatabase();
|
const defaultDatabase = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(defaultDB).toBeTruthy();
|
expect(defaultDatabase).toBeTruthy();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
DataOperator.handleIsolatedEntity({
|
operatorClient.handleIsolatedEntity({
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
tableName: 'INVALID_TABLE_NAME',
|
tableName: 'INVALID_TABLE_NAME',
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import {
|
||||||
getRangeOfValues,
|
getRangeOfValues,
|
||||||
getRawRecordPairs,
|
getRawRecordPairs,
|
||||||
getUniqueRawsBy,
|
getUniqueRawsBy,
|
||||||
hasSimilarUpdateAt,
|
getValidRecordsForUpdate,
|
||||||
retrieveRecords,
|
retrieveRecords,
|
||||||
} from '@database/operator/utils/general';
|
} from '@database/operator/utils/general';
|
||||||
import {
|
import {
|
||||||
|
|
@ -50,7 +50,7 @@ export interface BaseHandlerMix {
|
||||||
activeDatabase: Database;
|
activeDatabase: Database;
|
||||||
getActiveDatabase: () => DatabaseInstance;
|
getActiveDatabase: () => DatabaseInstance;
|
||||||
setActiveDatabase: (database: Database) => void;
|
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[]>;
|
handleEntityRecords: ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly}: HandleEntityRecordsArgs) => Promise<null | Model[]>;
|
||||||
processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>;
|
processInputs: ({rawValues, tableName, findMatchingRecordBy, fieldName}: ProcessInputsArgs) => Promise<{ createRaws: RecordPair[]; updateRaws: RecordPair[] }>;
|
||||||
batchOperations: ({database, models}: BatchOperationsArgs) => Promise<void>;
|
batchOperations: ({database, models}: BatchOperationsArgs) => Promise<void>;
|
||||||
|
|
@ -68,7 +68,7 @@ class BaseHandler {
|
||||||
*/
|
*/
|
||||||
activeDatabase: DatabaseInstance;
|
activeDatabase: DatabaseInstance;
|
||||||
|
|
||||||
constructor(serverDatabase?: Database) {
|
constructor(serverDatabase: Database) {
|
||||||
this.activeDatabase = serverDatabase;
|
this.activeDatabase = serverDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,13 +94,14 @@ class BaseHandler {
|
||||||
* @param {boolean} isolatedEntityArgs.prepareRecordsOnly
|
* @param {boolean} isolatedEntityArgs.prepareRecordsOnly
|
||||||
* @param {RawValue} isolatedEntityArgs.values
|
* @param {RawValue} isolatedEntityArgs.values
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Model[] | boolean}
|
* @returns {Model[]}
|
||||||
*/
|
*/
|
||||||
handleIsolatedEntity = async ({tableName, values, prepareRecordsOnly = true}: HandleIsolatedEntityArgs) => {
|
handleIsolatedEntity = async ({tableName, values, prepareRecordsOnly = true}: HandleIsolatedEntityArgs) => {
|
||||||
let findMatchingRecordBy;
|
let findMatchingRecordBy;
|
||||||
let fieldName;
|
let fieldName;
|
||||||
let operator;
|
let operator;
|
||||||
let rawValues;
|
let rawValues;
|
||||||
|
let records: Model[] = [];
|
||||||
|
|
||||||
if (!values.length) {
|
if (!values.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
|
|
@ -146,9 +147,9 @@ class BaseHandler {
|
||||||
}
|
}
|
||||||
case IsolatedEntities.SYSTEM: {
|
case IsolatedEntities.SYSTEM: {
|
||||||
findMatchingRecordBy = isRecordSystemEqualToRaw;
|
findMatchingRecordBy = isRecordSystemEqualToRaw;
|
||||||
fieldName = 'id';
|
fieldName = 'name';
|
||||||
operator = prepareSystemRecord;
|
operator = prepareSystemRecord;
|
||||||
rawValues = getUniqueRawsBy({raws: values, key: 'id'});
|
rawValues = getUniqueRawsBy({raws: values, key: 'name'});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case IsolatedEntities.TERMS_OF_SERVICE: {
|
case IsolatedEntities.TERMS_OF_SERVICE: {
|
||||||
|
|
@ -166,7 +167,7 @@ class BaseHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldName && findMatchingRecordBy) {
|
if (fieldName && findMatchingRecordBy) {
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName,
|
fieldName,
|
||||||
findMatchingRecordBy,
|
findMatchingRecordBy,
|
||||||
operator,
|
operator,
|
||||||
|
|
@ -175,10 +176,10 @@ class BaseHandler {
|
||||||
tableName,
|
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 {(DataFactoryArgs) => Promise<Model>} handleEntityArgs.operator
|
||||||
* @param {RawValue[]} handleEntityArgs.rawValues
|
* @param {RawValue[]} handleEntityArgs.rawValues
|
||||||
* @param {string} handleEntityArgs.tableName
|
* @param {string} handleEntityArgs.tableName
|
||||||
* @returns {Promise<null | Model[]>}
|
* @returns {Promise<Model[]>}
|
||||||
*/
|
*/
|
||||||
handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly = true}: HandleEntityRecordsArgs) => {
|
handleEntityRecords = async ({findMatchingRecordBy, fieldName, operator, rawValues, tableName, prepareRecordsOnly = true}: HandleEntityRecordsArgs) => {
|
||||||
if (!rawValues.length) {
|
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({
|
const {createRaws, updateRaws} = await this.processInputs({
|
||||||
rawValues,
|
rawValues,
|
||||||
tableName,
|
tableName,
|
||||||
|
|
@ -205,7 +207,8 @@ class BaseHandler {
|
||||||
|
|
||||||
const database = await this.getDatabase(tableName);
|
const database = await this.getDatabase(tableName);
|
||||||
|
|
||||||
const models = await this.prepareRecords({
|
let models: Model[] = [];
|
||||||
|
models = await this.prepareRecords({
|
||||||
database,
|
database,
|
||||||
tableName,
|
tableName,
|
||||||
createRaws,
|
createRaws,
|
||||||
|
|
@ -221,7 +224,7 @@ class BaseHandler {
|
||||||
await this.batchOperations({database, models});
|
await this.batchOperations({database, models});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return models;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -263,23 +266,17 @@ class BaseHandler {
|
||||||
const existingRecord = existingRecords[findIndex];
|
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
|
// 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,
|
tableName,
|
||||||
existingRecord,
|
existingRecord,
|
||||||
newValue: newElement,
|
newValue: newElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isUpdateAtSimilar) {
|
return updateRaws.push(updateRecords);
|
||||||
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 null;
|
|
||||||
|
// This RawValue is not present in the database; hence, we need to create it
|
||||||
|
return createRaws.push({record: undefined, raw: newElement});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -419,7 +416,8 @@ class BaseHandler {
|
||||||
* @returns {Promise<Database>}
|
* @returns {Promise<Database>}
|
||||||
*/
|
*/
|
||||||
getDefaultDatabase = async () => {
|
getDefaultDatabase = async () => {
|
||||||
const connection = await DatabaseManager.getDefaultDatabase();
|
const databaseManagerClient = new DatabaseManager();
|
||||||
|
const connection = await databaseManagerClient.getDefaultDatabase();
|
||||||
if (connection === undefined) {
|
if (connection === undefined) {
|
||||||
throw new DatabaseConnectionException(
|
throw new DatabaseConnectionException(
|
||||||
'An error occurred while retrieving the default database',
|
'An error occurred while retrieving the default database',
|
||||||
|
|
@ -440,16 +438,10 @@ class BaseHandler {
|
||||||
return this.activeDatabase;
|
return this.activeDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: here we are getting the active server directly as in a multi-server support system, the current
|
throw new DatabaseConnectionException(
|
||||||
// active server connection will already be set on application init
|
"This operator client didn't have its activeDatabase set",
|
||||||
const connection = await DatabaseManager.getActiveServerDatabase();
|
'',
|
||||||
if (connection === undefined) {
|
);
|
||||||
throw new DatabaseConnectionException(
|
|
||||||
'An error occurred while retrieving the server database',
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return connection;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {DataOperator} from '@database/operator';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import Operator from '@database/operator';
|
||||||
import {
|
import {
|
||||||
isRecordChannelEqualToRaw,
|
isRecordChannelEqualToRaw,
|
||||||
isRecordChannelInfoEqualToRaw,
|
isRecordChannelInfoEqualToRaw,
|
||||||
|
|
@ -15,20 +16,39 @@ import {
|
||||||
prepareMyChannelSettingsRecord,
|
prepareMyChannelSettingsRecord,
|
||||||
} from '@database/operator/prepareRecords/channel';
|
} from '@database/operator/prepareRecords/channel';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Operator: Channel Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleChannel: should write to CHANNEL entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleChannel({
|
await operatorClient.handleChannel({
|
||||||
channels: [
|
channels: [
|
||||||
{
|
{
|
||||||
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
|
id: 'kjlw9j1ttnxwig7tnqgebg7dtipno',
|
||||||
|
|
@ -89,11 +109,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
||||||
it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => {
|
it('=> HandleMyChannelSettings: should write to MY_CHANNEL_SETTINGS entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleMyChannelSettings({
|
await operatorClient.handleMyChannelSettings({
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
channel_id: 'c',
|
channel_id: 'c',
|
||||||
|
|
@ -138,11 +158,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
||||||
it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => {
|
it('=> HandleChannelInfo: should write to CHANNEL_INFO entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleChannelInfo({
|
await operatorClient.handleChannelInfo({
|
||||||
channelInfos: [
|
channelInfos: [
|
||||||
{
|
{
|
||||||
channel_id: 'c',
|
channel_id: 'c',
|
||||||
|
|
@ -180,11 +200,11 @@ describe('*** Operator: Channel Handlers tests ***', () => {
|
||||||
it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => {
|
it('=> HandleMyChannel: should write to MY_CHANNEL entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'channel_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleMyChannel({
|
await operatorClient.handleMyChannel({
|
||||||
myChannels: [
|
myChannels: [
|
||||||
{
|
{
|
||||||
channel_id: 'c',
|
channel_id: 'c',
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,11 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawChannel[]} channelsArgs.channels
|
* @param {RawChannel[]} channelsArgs.channels
|
||||||
* @param {boolean} channelsArgs.prepareRecordsOnly
|
* @param {boolean} channelsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Channel[]|boolean}
|
* @returns {Channel[]}
|
||||||
*/
|
*/
|
||||||
handleChannel = async ({channels, prepareRecordsOnly = true}: HandleChannelArgs) => {
|
handleChannel = async ({channels, prepareRecordsOnly = true}: HandleChannelArgs) => {
|
||||||
|
let records: Channel[] = [];
|
||||||
|
|
||||||
if (!channels.length) {
|
if (!channels.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "channels" array has been passed to the handleChannel method',
|
'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 rawValues = getUniqueRawsBy({raws: channels, key: 'id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
findMatchingRecordBy: isRecordChannelEqualToRaw,
|
findMatchingRecordBy: isRecordChannelEqualToRaw,
|
||||||
operator: prepareChannelRecord,
|
operator: prepareChannelRecord,
|
||||||
|
|
@ -68,7 +70,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: CHANNEL,
|
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 {RawMyChannelSettings[]} settingsArgs.settings
|
||||||
* @param {boolean} settingsArgs.prepareRecordsOnly
|
* @param {boolean} settingsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {MyChannelSettings[]| boolean}
|
* @returns {MyChannelSettings[]}
|
||||||
*/
|
*/
|
||||||
handleMyChannelSettings = async ({settings, prepareRecordsOnly = true}: HandleMyChannelSettingsArgs) => {
|
handleMyChannelSettings = async ({settings, prepareRecordsOnly = true}: HandleMyChannelSettingsArgs) => {
|
||||||
|
let records: MyChannelSettings[] = [];
|
||||||
|
|
||||||
if (!settings.length) {
|
if (!settings.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "settings" array has been passed to the handleMyChannelSettings method',
|
'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 rawValues = getUniqueRawsBy({raws: settings, key: 'channel_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'channel_id',
|
fieldName: 'channel_id',
|
||||||
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
|
findMatchingRecordBy: isRecordMyChannelSettingsEqualToRaw,
|
||||||
operator: prepareMyChannelSettingsRecord,
|
operator: prepareMyChannelSettingsRecord,
|
||||||
|
|
@ -97,7 +101,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: MY_CHANNEL_SETTINGS,
|
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 {RawChannelInfo[]} channelInfosArgs.channelInfos
|
||||||
* @param {boolean} channelInfosArgs.prepareRecordsOnly
|
* @param {boolean} channelInfosArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {ChannelInfo[]| boolean}
|
* @returns {ChannelInfo[]}
|
||||||
*/
|
*/
|
||||||
handleChannelInfo = async ({channelInfos, prepareRecordsOnly = true}: HandleChannelInfoArgs) => {
|
handleChannelInfo = async ({channelInfos, prepareRecordsOnly = true}: HandleChannelInfoArgs) => {
|
||||||
|
let records: ChannelInfo[] = [];
|
||||||
|
|
||||||
if (!channelInfos.length) {
|
if (!channelInfos.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "channelInfos" array has been passed to the handleMyChannelSettings method',
|
'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',
|
key: 'channel_id',
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'channel_id',
|
fieldName: 'channel_id',
|
||||||
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
|
findMatchingRecordBy: isRecordChannelInfoEqualToRaw,
|
||||||
operator: prepareChannelInfoRecord,
|
operator: prepareChannelInfoRecord,
|
||||||
|
|
@ -129,7 +135,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: CHANNEL_INFO,
|
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 {RawMyChannel[]} myChannelsArgs.myChannels
|
||||||
* @param {boolean} myChannelsArgs.prepareRecordsOnly
|
* @param {boolean} myChannelsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {MyChannel[]| boolean}
|
* @returns {MyChannel[]}
|
||||||
*/
|
*/
|
||||||
handleMyChannel = async ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs) => {
|
handleMyChannel = async ({myChannels, prepareRecordsOnly = true}: HandleMyChannelArgs) => {
|
||||||
|
let records: MyChannel[] = [];
|
||||||
|
|
||||||
if (!myChannels.length) {
|
if (!myChannels.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "myChannels" array has been passed to the handleMyChannel method',
|
'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',
|
key: 'channel_id',
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'channel_id',
|
fieldName: 'channel_id',
|
||||||
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
|
findMatchingRecordBy: isRecordMyChannelEqualToRaw,
|
||||||
operator: prepareMyChannelRecord,
|
operator: prepareMyChannelRecord,
|
||||||
|
|
@ -161,7 +169,7 @@ const ChannelHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: MY_CHANNEL,
|
tableName: MY_CHANNEL,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prepareRecordsOnly && records?.length && records;
|
return records;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {DataOperator} from '@database/operator';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import Operator from '@database/operator';
|
||||||
import {
|
import {
|
||||||
isRecordGroupEqualToRaw,
|
isRecordGroupEqualToRaw,
|
||||||
isRecordGroupMembershipEqualToRaw,
|
isRecordGroupMembershipEqualToRaw,
|
||||||
|
|
@ -15,20 +16,39 @@ import {
|
||||||
prepareGroupsInTeamRecord,
|
prepareGroupsInTeamRecord,
|
||||||
} from '@database/operator/prepareRecords/group';
|
} from '@database/operator/prepareRecords/group';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Operator: Group Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleGroup: should write to GROUP entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleGroup({
|
await operatorClient.handleGroup({
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
id: 'id_groupdfjdlfkjdkfdsf',
|
id: 'id_groupdfjdlfkjdkfdsf',
|
||||||
|
|
@ -73,11 +93,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
||||||
it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => {
|
it('=> HandleGroupsInTeam: should write to GROUPS_IN_TEAM entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleGroupsInTeam({
|
await operatorClient.handleGroupsInTeam({
|
||||||
groupsInTeams: [
|
groupsInTeams: [
|
||||||
{
|
{
|
||||||
team_id: 'team_899',
|
team_id: 'team_899',
|
||||||
|
|
@ -118,11 +138,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
||||||
it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => {
|
it('=> HandleGroupsInChannel: should write to GROUPS_IN_CHANNEL entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleGroupsInChannel({
|
await operatorClient.handleGroupsInChannel({
|
||||||
groupsInChannels: [
|
groupsInChannels: [
|
||||||
{
|
{
|
||||||
auto_add: true,
|
auto_add: true,
|
||||||
|
|
@ -173,11 +193,11 @@ describe('*** Operator: Group Handlers tests ***', () => {
|
||||||
it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => {
|
it('=> HandleGroupMembership: should write to GROUP_MEMBERSHIP entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'group_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleGroupMembership({
|
await operatorClient.handleGroupMembership({
|
||||||
groupMemberships: [
|
groupMemberships: [
|
||||||
{
|
{
|
||||||
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
|
user_id: 'u4cprpki7ri81mbx8efixcsb8jo',
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,11 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
|
* @param {RawGroupMembership[]} groupMembershipsArgs.groupMemberships
|
||||||
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
|
* @param {boolean} groupMembershipsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {GroupMembership[] | boolean}
|
* @returns {GroupMembership[]}
|
||||||
*/
|
*/
|
||||||
handleGroupMembership = async ({groupMemberships, prepareRecordsOnly = true}: HandleGroupMembershipArgs) => {
|
handleGroupMembership = async ({groupMemberships, prepareRecordsOnly = true}: HandleGroupMembershipArgs) => {
|
||||||
|
let records: GroupMembership[] = [];
|
||||||
|
|
||||||
if (!groupMemberships.length) {
|
if (!groupMemberships.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "groupMemberships" array has been passed to the handleGroupMembership method',
|
'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 rawValues = getUniqueRawsBy({raws: groupMemberships, key: 'group_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'user_id',
|
fieldName: 'user_id',
|
||||||
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
|
findMatchingRecordBy: isRecordGroupMembershipEqualToRaw,
|
||||||
operator: prepareGroupMembershipRecord,
|
operator: prepareGroupMembershipRecord,
|
||||||
|
|
@ -68,7 +70,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: GROUP_MEMBERSHIP,
|
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 {RawGroup[]} groupsArgs.groups
|
||||||
* @param {boolean} groupsArgs.prepareRecordsOnly
|
* @param {boolean} groupsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Group[] | boolean}
|
* @returns {Group[]}
|
||||||
*/
|
*/
|
||||||
handleGroup = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs) => {
|
handleGroup = async ({groups, prepareRecordsOnly = true}: HandleGroupArgs) => {
|
||||||
|
let records: Group[] = [];
|
||||||
|
|
||||||
if (!groups.length) {
|
if (!groups.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "groups" array has been passed to the handleGroup method',
|
'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 rawValues = getUniqueRawsBy({raws: groups, key: 'name'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'name',
|
fieldName: 'name',
|
||||||
findMatchingRecordBy: isRecordGroupEqualToRaw,
|
findMatchingRecordBy: isRecordGroupEqualToRaw,
|
||||||
operator: prepareGroupRecord,
|
operator: prepareGroupRecord,
|
||||||
|
|
@ -97,7 +101,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: GROUP,
|
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 {RawGroupsInTeam[]} groupsInTeamsArgs.groupsInTeams
|
||||||
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
|
* @param {boolean} groupsInTeamsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {GroupsInTeam[] | boolean}
|
* @returns {GroupsInTeam[]}
|
||||||
*/
|
*/
|
||||||
handleGroupsInTeam = async ({groupsInTeams, prepareRecordsOnly = true} : HandleGroupsInTeamArgs) => {
|
handleGroupsInTeam = async ({groupsInTeams, prepareRecordsOnly = true} : HandleGroupsInTeamArgs) => {
|
||||||
|
let records: GroupsInTeam[] = [];
|
||||||
|
|
||||||
if (!groupsInTeams.length) {
|
if (!groupsInTeams.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "groups" array has been passed to the handleGroupsInTeam method',
|
'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 rawValues = getUniqueRawsBy({raws: groupsInTeams, key: 'group_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'group_id',
|
fieldName: 'group_id',
|
||||||
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
|
findMatchingRecordBy: isRecordGroupsInTeamEqualToRaw,
|
||||||
operator: prepareGroupsInTeamRecord,
|
operator: prepareGroupsInTeamRecord,
|
||||||
|
|
@ -126,7 +132,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: GROUPS_IN_TEAM,
|
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 {RawGroupsInChannel[]} groupsInChannelsArgs.groupsInChannels
|
||||||
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
|
* @param {boolean} groupsInChannelsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {GroupsInChannel[] | boolean}
|
* @returns {GroupsInChannel[]}
|
||||||
*/
|
*/
|
||||||
handleGroupsInChannel = async ({groupsInChannels, prepareRecordsOnly = true}: HandleGroupsInChannelArgs) => {
|
handleGroupsInChannel = async ({groupsInChannels, prepareRecordsOnly = true}: HandleGroupsInChannelArgs) => {
|
||||||
|
let records: GroupsInChannel[] = [];
|
||||||
|
|
||||||
if (!groupsInChannels.length) {
|
if (!groupsInChannels.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "groups" array has been passed to the handleGroupsInTeam method',
|
'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 rawValues = getUniqueRawsBy({raws: groupsInChannels, key: 'channel_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'group_id',
|
fieldName: 'group_id',
|
||||||
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
|
findMatchingRecordBy: isRecordGroupsInChannelEqualToRaw,
|
||||||
operator: prepareGroupsInChannelRecord,
|
operator: prepareGroupsInChannelRecord,
|
||||||
|
|
@ -155,7 +163,7 @@ const GroupHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: GROUPS_IN_CHANNEL,
|
tableName: GROUPS_IN_CHANNEL,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prepareRecordsOnly && records?.length && records;
|
return records;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,48 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {isRecordDraftEqualToRaw} from '@database/operator/comparators';
|
||||||
import {prepareDraftRecord} from '@database/operator/prepareRecords/post';
|
import {prepareDraftRecord} from '@database/operator/prepareRecords/post';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Operator: Post Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleDraft: should write to the Draft entity', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'post_handler', setActive: true});
|
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 = [
|
const values = [
|
||||||
{
|
{
|
||||||
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
|
channel_id: '4r9jmr7eqt8dxq3f9woypzurrychannelid',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
|
id: '322dxx',
|
||||||
user_id: 'user_id',
|
user_id: 'user_id',
|
||||||
post_id: 'post_id',
|
post_id: 'post_id',
|
||||||
create_at: 123,
|
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({
|
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||||
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
||||||
|
|
@ -206,17 +227,17 @@ describe('*** Operator: Post Handlers tests ***', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const spyOnHandleFiles = jest.spyOn(DataOperator as any, 'handleFiles');
|
const spyOnHandleFiles = jest.spyOn(operatorClient as any, 'handleFiles');
|
||||||
const spyOnHandlePostMetadata = jest.spyOn(DataOperator as any, 'handlePostMetadata');
|
const spyOnHandlePostMetadata = jest.spyOn(operatorClient as any, 'handlePostMetadata');
|
||||||
const spyOnHandleReactions = jest.spyOn(DataOperator as any, 'handleReactions');
|
const spyOnHandleReactions = jest.spyOn(operatorClient as any, 'handleReactions');
|
||||||
const spyOnHandleCustomEmojis = jest.spyOn(DataOperator as any, 'handleIsolatedEntity');
|
const spyOnHandleCustomEmojis = jest.spyOn(operatorClient as any, 'handleIsolatedEntity');
|
||||||
const spyOnHandlePostsInThread = jest.spyOn(DataOperator as any, 'handlePostsInThread');
|
const spyOnHandlePostsInThread = jest.spyOn(operatorClient as any, 'handlePostsInThread');
|
||||||
const spyOnHandlePostsInChannel = jest.spyOn(DataOperator as any, 'handlePostsInChannel');
|
const spyOnHandlePostsInChannel = jest.spyOn(operatorClient as any, 'handlePostsInChannel');
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'post_handler', setActive: true});
|
await createTestConnection({databaseName: 'post_handler', setActive: true});
|
||||||
|
|
||||||
// handlePosts will in turn call handlePostsInThread
|
// handlePosts will in turn call handlePostsInThread
|
||||||
await DataOperator.handlePosts({
|
await operatorClient.handlePosts({
|
||||||
orders: [
|
orders: [
|
||||||
'8swgtrrdiff89jnsiwiip3y1eoe',
|
'8swgtrrdiff89jnsiwiip3y1eoe',
|
||||||
'8fcnk3p1jt8mmkaprgajoxz115a',
|
'8fcnk3p1jt8mmkaprgajoxz115a',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Q} from '@nozbe/watermelondb';
|
||||||
|
import Model from '@nozbe/watermelondb/Model';
|
||||||
|
|
||||||
import {MM_TABLES} from '@constants/database';
|
import {MM_TABLES} from '@constants/database';
|
||||||
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
import DataOperatorException from '@database/exceptions/data_operator_exception';
|
||||||
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/comparators';
|
import {isRecordDraftEqualToRaw, isRecordPostEqualToRaw} from '@database/operator/comparators';
|
||||||
|
|
@ -14,8 +17,6 @@ import {
|
||||||
} from '@database/operator/prepareRecords/post';
|
} from '@database/operator/prepareRecords/post';
|
||||||
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
|
import {getRawRecordPairs, getUniqueRawsBy, retrieveRecords} from '@database/operator/utils/general';
|
||||||
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
|
import {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
|
||||||
import {Q} from '@nozbe/watermelondb';
|
|
||||||
import Model from '@nozbe/watermelondb/Model';
|
|
||||||
import {
|
import {
|
||||||
HandleDraftArgs,
|
HandleDraftArgs,
|
||||||
HandleFilesArgs,
|
HandleFilesArgs,
|
||||||
|
|
@ -64,9 +65,11 @@ const PostHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawDraft[]} draftsArgs.drafts
|
* @param {RawDraft[]} draftsArgs.drafts
|
||||||
* @param {boolean} draftsArgs.prepareRecordsOnly
|
* @param {boolean} draftsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Draft[] | boolean}
|
* @returns {Draft[]}
|
||||||
*/
|
*/
|
||||||
handleDraft = async ({drafts, prepareRecordsOnly = true}: HandleDraftArgs) => {
|
handleDraft = async ({drafts, prepareRecordsOnly = true}: HandleDraftArgs) => {
|
||||||
|
let records: Draft[] = [];
|
||||||
|
|
||||||
if (!drafts.length) {
|
if (!drafts.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "drafts" array has been passed to the handleReactions method',
|
'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 rawValues = getUniqueRawsBy({raws: drafts, key: 'channel_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'channel_id',
|
fieldName: 'channel_id',
|
||||||
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
findMatchingRecordBy: isRecordDraftEqualToRaw,
|
||||||
operator: prepareDraftRecord,
|
operator: prepareDraftRecord,
|
||||||
|
|
@ -84,7 +87,7 @@ const PostHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: DRAFT,
|
tableName: DRAFT,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prepareRecordsOnly && records?.length && records;
|
return records;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {DataOperator} from '@database/operator';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import Operator from '@database/operator';
|
||||||
import {
|
import {
|
||||||
isRecordMyTeamEqualToRaw,
|
isRecordMyTeamEqualToRaw,
|
||||||
isRecordSlashCommandEqualToRaw,
|
isRecordSlashCommandEqualToRaw,
|
||||||
|
|
@ -19,20 +20,39 @@ import {
|
||||||
prepareTeamSearchHistoryRecord,
|
prepareTeamSearchHistoryRecord,
|
||||||
} from '@database/operator/prepareRecords/team';
|
} from '@database/operator/prepareRecords/team';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Operator: Team Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleTeam: should write to TEAM entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleTeam({
|
await operatorClient.handleTeam({
|
||||||
teams: [
|
teams: [
|
||||||
{
|
{
|
||||||
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
|
id: 'rcgiyftm7jyrxnmdfdfa1osd8zswby',
|
||||||
|
|
@ -89,11 +109,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
||||||
it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => {
|
it('=> HandleTeamMemberships: should write to TEAM_MEMBERSHIP entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleTeamMemberships({
|
await operatorClient.handleTeamMemberships({
|
||||||
teamMemberships: [
|
teamMemberships: [
|
||||||
{
|
{
|
||||||
team_id: 'a',
|
team_id: 'a',
|
||||||
|
|
@ -134,11 +154,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
||||||
it('=> HandleMyTeam: should write to MY_TEAM entity', async () => {
|
it('=> HandleMyTeam: should write to MY_TEAM entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleMyTeam({
|
await operatorClient.handleMyTeam({
|
||||||
myTeams: [
|
myTeams: [
|
||||||
{
|
{
|
||||||
team_id: 'teamA',
|
team_id: 'teamA',
|
||||||
|
|
@ -171,11 +191,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
||||||
it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => {
|
it('=> HandleTeamChannelHistory: should write to TEAM_CHANNEL_HISTORY entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleTeamChannelHistory({
|
await operatorClient.handleTeamChannelHistory({
|
||||||
teamChannelHistories: [
|
teamChannelHistories: [
|
||||||
{
|
{
|
||||||
team_id: 'a',
|
team_id: 'a',
|
||||||
|
|
@ -199,11 +219,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
||||||
it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => {
|
it('=> HandleTeamSearchHistory: should write to TEAM_SEARCH_HISTORY entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleTeamSearchHistory({
|
await operatorClient.handleTeamSearchHistory({
|
||||||
teamSearchHistories: [
|
teamSearchHistories: [
|
||||||
{
|
{
|
||||||
team_id: 'a',
|
team_id: 'a',
|
||||||
|
|
@ -236,11 +256,11 @@ describe('*** Operator: Team Handlers tests ***', () => {
|
||||||
it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => {
|
it('=> HandleSlashCommand: should write to SLASH_COMMAND entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'team_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleSlashCommand({
|
await operatorClient.handleSlashCommand({
|
||||||
slashCommands: [
|
slashCommands: [
|
||||||
{
|
{
|
||||||
id: 'command_1',
|
id: 'command_1',
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import SlashCommand from '@typings/database/slash_command';
|
||||||
import Team from '@typings/database/team';
|
import Team from '@typings/database/team';
|
||||||
import TeamChannelHistory from '@typings/database/team_channel_history';
|
import TeamChannelHistory from '@typings/database/team_channel_history';
|
||||||
import TeamMembership from '@typings/database/team_membership';
|
import TeamMembership from '@typings/database/team_membership';
|
||||||
|
import TeamSearchHistory from '@typings/database/team_search_history';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
MY_TEAM,
|
MY_TEAM,
|
||||||
|
|
@ -44,11 +45,11 @@ const {
|
||||||
} = MM_TABLES.SERVER;
|
} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export interface TeamHandlerMix {
|
export interface TeamHandlerMix {
|
||||||
handleTeamMemberships : ({teamMemberships, prepareRecordsOnly}: HandleTeamMembershipArgs) => TeamMembership[] | boolean,
|
handleTeamMemberships: ({teamMemberships, prepareRecordsOnly}: HandleTeamMembershipArgs) => TeamMembership[];
|
||||||
handleTeam: ({teams, prepareRecordsOnly}: HandleTeamArgs) => Team[] | boolean
|
handleTeam: ({teams, prepareRecordsOnly}: HandleTeamArgs) => Team[];
|
||||||
handleTeamChannelHistory : ({teamChannelHistories, prepareRecordsOnly}: HandleTeamChannelHistoryArgs) => TeamChannelHistory[]| boolean,
|
handleTeamChannelHistory: ({teamChannelHistories, prepareRecordsOnly}: HandleTeamChannelHistoryArgs) => TeamChannelHistory[];
|
||||||
handleSlashCommand : ({slashCommands, prepareRecordsOnly} : HandleSlashCommandArgs) => SlashCommand[]| boolean,
|
handleSlashCommand: ({slashCommands, prepareRecordsOnly}: HandleSlashCommandArgs) => SlashCommand[];
|
||||||
handleMyTeam : ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => MyTeam[]| boolean
|
handleMyTeam: ({myTeams, prepareRecordsOnly}: HandleMyTeamArgs) => MyTeam[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamHandler = (superclass: any) => class extends superclass {
|
const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
|
|
@ -58,9 +59,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawTeamMembership[]} teamMembershipsArgs.teamMemberships
|
* @param {RawTeamMembership[]} teamMembershipsArgs.teamMemberships
|
||||||
* @param {boolean} teamMembershipsArgs.prepareRecordsOnly
|
* @param {boolean} teamMembershipsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {TeamMembership[] | boolean}
|
* @returns {TeamMembership[]}
|
||||||
*/
|
*/
|
||||||
handleTeamMemberships = async ({teamMemberships, prepareRecordsOnly = true}: HandleTeamMembershipArgs) => {
|
handleTeamMemberships = async ({teamMemberships, prepareRecordsOnly = true}: HandleTeamMembershipArgs) => {
|
||||||
|
let records: TeamMembership[] = [];
|
||||||
|
|
||||||
if (!teamMemberships.length) {
|
if (!teamMemberships.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "teamMemberships" array has been passed to the handleTeamMemberships method',
|
'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 rawValues = getUniqueRawsBy({raws: teamMemberships, key: 'team_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'user_id',
|
fieldName: 'user_id',
|
||||||
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
|
findMatchingRecordBy: isRecordTeamMembershipEqualToRaw,
|
||||||
operator: prepareTeamMembershipRecord,
|
operator: prepareTeamMembershipRecord,
|
||||||
|
|
@ -78,7 +81,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
prepareRecordsOnly,
|
prepareRecordsOnly,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prepareRecordsOnly && records?.length && records;
|
return records;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -87,9 +90,11 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawTeam[]} teamsArgs.teams
|
* @param {RawTeam[]} teamsArgs.teams
|
||||||
* @param {boolean} teamsArgs.prepareRecordsOnly
|
* @param {boolean} teamsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Team[] | boolean}
|
* @returns {Team[]}
|
||||||
*/
|
*/
|
||||||
handleTeam = async ({teams, prepareRecordsOnly = true}: HandleTeamArgs) => {
|
handleTeam = async ({teams, prepareRecordsOnly = true}: HandleTeamArgs) => {
|
||||||
|
let records: Team[] = [];
|
||||||
|
|
||||||
if (!teams.length) {
|
if (!teams.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "teams" array has been passed to the handleTeam method',
|
'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 rawValues = getUniqueRawsBy({raws: teams, key: 'id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
findMatchingRecordBy: isRecordTeamEqualToRaw,
|
findMatchingRecordBy: isRecordTeamEqualToRaw,
|
||||||
operator: prepareTeamRecord,
|
operator: prepareTeamRecord,
|
||||||
|
|
@ -107,7 +112,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: TEAM,
|
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 {RawTeamChannelHistory[]} teamChannelHistoriesArgs.teamChannelHistories
|
||||||
* @param {boolean} teamChannelHistoriesArgs.prepareRecordsOnly
|
* @param {boolean} teamChannelHistoriesArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {TeamChannelHistory[]| boolean}
|
* @returns {TeamChannelHistory[]}
|
||||||
*/
|
*/
|
||||||
handleTeamChannelHistory = async ({teamChannelHistories, prepareRecordsOnly = true}: HandleTeamChannelHistoryArgs) => {
|
handleTeamChannelHistory = async ({teamChannelHistories, prepareRecordsOnly = true}: HandleTeamChannelHistoryArgs) => {
|
||||||
|
let records: TeamChannelHistory[] = [];
|
||||||
|
|
||||||
if (!teamChannelHistories.length) {
|
if (!teamChannelHistories.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "teamChannelHistories" array has been passed to the handleTeamChannelHistory method',
|
'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 rawValues = getUniqueRawsBy({raws: teamChannelHistories, key: 'team_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'team_id',
|
fieldName: 'team_id',
|
||||||
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
|
findMatchingRecordBy: isRecordTeamChannelHistoryEqualToRaw,
|
||||||
operator: prepareTeamChannelHistoryRecord,
|
operator: prepareTeamChannelHistoryRecord,
|
||||||
|
|
@ -136,7 +143,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: TEAM_CHANNEL_HISTORY,
|
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 {RawTeamSearchHistory[]} teamSearchHistoriesArgs.teamSearchHistories
|
||||||
* @param {boolean} teamSearchHistoriesArgs.prepareRecordsOnly
|
* @param {boolean} teamSearchHistoriesArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {TeamSearchHistory[]| boolean}
|
* @returns {TeamSearchHistory[]}
|
||||||
*/
|
*/
|
||||||
handleTeamSearchHistory = async ({teamSearchHistories, prepareRecordsOnly = true}: HandleTeamSearchHistoryArgs) => {
|
handleTeamSearchHistory = async ({teamSearchHistories, prepareRecordsOnly = true}: HandleTeamSearchHistoryArgs) => {
|
||||||
|
let records: TeamSearchHistory[] = [];
|
||||||
|
|
||||||
if (!teamSearchHistories.length) {
|
if (!teamSearchHistories.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "teamSearchHistories" array has been passed to the handleTeamSearchHistory method',
|
'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 rawValues = getUniqueRawsBy({raws: teamSearchHistories, key: 'term'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'team_id',
|
fieldName: 'team_id',
|
||||||
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
|
findMatchingRecordBy: isRecordTeamSearchHistoryEqualToRaw,
|
||||||
operator: prepareTeamSearchHistoryRecord,
|
operator: prepareTeamSearchHistoryRecord,
|
||||||
|
|
@ -165,7 +174,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: TEAM_SEARCH_HISTORY,
|
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 {RawSlashCommand[]} slashCommandsArgs.slashCommands
|
||||||
* @param {boolean} slashCommandsArgs.prepareRecordsOnly
|
* @param {boolean} slashCommandsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @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) {
|
if (!slashCommands.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "slashCommands" array has been passed to the handleSlashCommand method',
|
'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 rawValues = getUniqueRawsBy({raws: slashCommands, key: 'id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
|
findMatchingRecordBy: isRecordSlashCommandEqualToRaw,
|
||||||
operator: prepareSlashCommandRecord,
|
operator: prepareSlashCommandRecord,
|
||||||
|
|
@ -194,7 +205,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: SLASH_COMMAND,
|
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 {RawMyTeam[]} myTeamsArgs.myTeams
|
||||||
* @param {boolean} myTeamsArgs.prepareRecordsOnly
|
* @param {boolean} myTeamsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {MyTeam[]| boolean}
|
* @returns {MyTeam[]}
|
||||||
*/
|
*/
|
||||||
handleMyTeam = async ({myTeams, prepareRecordsOnly = true}: HandleMyTeamArgs) => {
|
handleMyTeam = async ({myTeams, prepareRecordsOnly = true}: HandleMyTeamArgs) => {
|
||||||
|
let records: MyTeam[] = [];
|
||||||
|
|
||||||
if (!myTeams.length) {
|
if (!myTeams.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "myTeams" array has been passed to the handleSlashCommand method',
|
'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 rawValues = getUniqueRawsBy({raws: myTeams, key: 'team_id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'team_id',
|
fieldName: 'team_id',
|
||||||
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
|
findMatchingRecordBy: isRecordMyTeamEqualToRaw,
|
||||||
operator: prepareMyTeamRecord,
|
operator: prepareMyTeamRecord,
|
||||||
|
|
@ -223,7 +236,7 @@ const TeamHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: MY_TEAM,
|
tableName: MY_TEAM,
|
||||||
});
|
});
|
||||||
|
|
||||||
return prepareRecordsOnly && records?.length && records;
|
return records;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {DataOperator} from '@database/operator';
|
import DatabaseManager from '@database/manager';
|
||||||
|
import Operator from '@database/operator';
|
||||||
import {
|
import {
|
||||||
isRecordChannelMembershipEqualToRaw,
|
isRecordChannelMembershipEqualToRaw,
|
||||||
isRecordPreferenceEqualToRaw,
|
isRecordPreferenceEqualToRaw,
|
||||||
|
|
@ -13,21 +14,40 @@ import {
|
||||||
prepareUserRecord,
|
prepareUserRecord,
|
||||||
} from '@database/operator/prepareRecords/user';
|
} from '@database/operator/prepareRecords/user';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** Operator: User Handlers tests ***', () => {
|
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 () => {
|
it('=> HandleReactions: should write to both Reactions and CustomEmoji entities', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
|
|
||||||
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
await createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||||
|
|
||||||
const spyOnPrepareRecords = jest.spyOn(DataOperator as any, 'prepareRecords');
|
const spyOnPrepareRecords = jest.spyOn(operatorClient as any, 'prepareRecords');
|
||||||
const spyOnBatchOperation = jest.spyOn(DataOperator as any, 'batchOperations');
|
const spyOnBatchOperation = jest.spyOn(operatorClient as any, 'batchOperations');
|
||||||
|
|
||||||
await DataOperator.handleReactions({
|
await operatorClient.handleReactions({
|
||||||
reactions: [
|
reactions: [
|
||||||
{
|
{
|
||||||
create_at: 1608263728086,
|
create_at: 1608263728086,
|
||||||
|
|
@ -88,16 +108,16 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||||
timezone: {
|
timezone: {
|
||||||
automaticTimezone: 'Indian/Mauritius',
|
automaticTimezone: 'Indian/Mauritius',
|
||||||
manualTimezone: '',
|
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 createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleUsers({users, prepareRecordsOnly: false});
|
await operatorClient.handleUsers({users, prepareRecordsOnly: false});
|
||||||
|
|
||||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
|
expect(spyOnHandleEntityRecords).toHaveBeenCalledTimes(1);
|
||||||
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
expect(spyOnHandleEntityRecords).toHaveBeenCalledWith({
|
||||||
|
|
@ -139,7 +159,7 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||||
timezone: {
|
timezone: {
|
||||||
automaticTimezone: 'Indian/Mauritius',
|
automaticTimezone: 'Indian/Mauritius',
|
||||||
manualTimezone: '',
|
manualTimezone: '',
|
||||||
useAutomaticTimezone: true,
|
useAutomaticTimezone: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -153,11 +173,11 @@ describe('*** Operator: User Handlers tests ***', () => {
|
||||||
it('=> HandlePreferences: should write to PREFERENCE entity', async () => {
|
it('=> HandlePreferences: should write to PREFERENCE entity', async () => {
|
||||||
expect.assertions(2);
|
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 createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handlePreferences({
|
await operatorClient.handlePreferences({
|
||||||
preferences: [
|
preferences: [
|
||||||
{
|
{
|
||||||
user_id: '9ciscaqbrpd6d8s68k76xb9bte',
|
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 createTestConnection({databaseName: 'user_handler', setActive: true});
|
||||||
|
|
||||||
await DataOperator.handleChannelMembership({
|
await operatorClient.handleChannelMembership({
|
||||||
channelMemberships,
|
channelMemberships,
|
||||||
prepareRecordsOnly: false,
|
prepareRecordsOnly: false,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import {
|
||||||
} from '@database/operator/prepareRecords/user';
|
} from '@database/operator/prepareRecords/user';
|
||||||
import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general';
|
import {getRawRecordPairs, getUniqueRawsBy} from '@database/operator/utils/general';
|
||||||
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
||||||
import Model from '@nozbe/watermelondb/Model';
|
|
||||||
import ChannelMembership from '@typings/database/channel_membership';
|
import ChannelMembership from '@typings/database/channel_membership';
|
||||||
import CustomEmoji from '@typings/database/custom_emoji';
|
import CustomEmoji from '@typings/database/custom_emoji';
|
||||||
import {
|
import {
|
||||||
|
|
@ -40,10 +39,10 @@ const {
|
||||||
} = MM_TABLES.SERVER;
|
} = MM_TABLES.SERVER;
|
||||||
|
|
||||||
export interface UserHandlerMix {
|
export interface UserHandlerMix {
|
||||||
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[] | boolean,
|
handleChannelMembership : ({channelMemberships, prepareRecordsOnly}: HandleChannelMembershipArgs) => ChannelMembership[];
|
||||||
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[] | boolean,
|
handlePreferences : ({preferences, prepareRecordsOnly}: HandlePreferencesArgs) => Preference[];
|
||||||
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => boolean | (Reaction | CustomEmoji)[],
|
handleReactions : ({reactions, prepareRecordsOnly}: HandleReactionsArgs) =>(Reaction | CustomEmoji)[];
|
||||||
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[] | boolean
|
handleUsers : ({users, prepareRecordsOnly}: HandleUsersArgs) => User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserHandler = (superclass: any) => class extends superclass {
|
const UserHandler = (superclass: any) => class extends superclass {
|
||||||
|
|
@ -53,21 +52,20 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||||
* @param {RawChannelMembership[]} channelMembershipsArgs.channelMemberships
|
* @param {RawChannelMembership[]} channelMembershipsArgs.channelMemberships
|
||||||
* @param {boolean} channelMembershipsArgs.prepareRecordsOnly
|
* @param {boolean} channelMembershipsArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {ChannelMembership[] | boolean}
|
* @returns {ChannelMembership[]}
|
||||||
*/
|
*/
|
||||||
handleChannelMembership = async ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs) => {
|
handleChannelMembership = async ({channelMemberships, prepareRecordsOnly = true}: HandleChannelMembershipArgs) => {
|
||||||
|
let records: ChannelMembership[] = [];
|
||||||
|
|
||||||
if (!channelMemberships.length) {
|
if (!channelMemberships.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "channelMemberships" array has been passed to the handleChannelMembership method',
|
'An empty "channelMemberships" array has been passed to the handleChannelMembership method',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawValues = getUniqueRawsBy({
|
const rawValues = getUniqueRawsBy({raws: channelMemberships, key: 'channel_id'});
|
||||||
raws: channelMemberships,
|
|
||||||
key: 'channel_id',
|
|
||||||
});
|
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'user_id',
|
fieldName: 'user_id',
|
||||||
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
|
findMatchingRecordBy: isRecordChannelMembershipEqualToRaw,
|
||||||
operator: prepareChannelMembershipRecord,
|
operator: prepareChannelMembershipRecord,
|
||||||
|
|
@ -76,7 +74,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: CHANNEL_MEMBERSHIP,
|
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 {RawPreference[]} preferencesArgs.preferences
|
||||||
* @param {boolean} preferencesArgs.prepareRecordsOnly
|
* @param {boolean} preferencesArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {Preference[] | boolean}
|
* @returns {Preference[]}
|
||||||
*/
|
*/
|
||||||
handlePreferences = async ({preferences, prepareRecordsOnly = true}: HandlePreferencesArgs) => {
|
handlePreferences = async ({preferences, prepareRecordsOnly = true}: HandlePreferencesArgs) => {
|
||||||
|
let records: Preference[] = [];
|
||||||
|
|
||||||
if (!preferences.length) {
|
if (!preferences.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "preferences" array has been passed to the handlePreferences method',
|
'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 rawValues = getUniqueRawsBy({raws: preferences, key: 'name'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'user_id',
|
fieldName: 'user_id',
|
||||||
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
|
findMatchingRecordBy: isRecordPreferenceEqualToRaw,
|
||||||
operator: preparePreferenceRecord,
|
operator: preparePreferenceRecord,
|
||||||
|
|
@ -105,7 +105,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||||
tableName: PREFERENCE,
|
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 {RawReaction[]} handleReactions.reactions
|
||||||
* @param {boolean} handleReactions.prepareRecordsOnly
|
* @param {boolean} handleReactions.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {boolean | (Reaction | CustomEmoji)[]}
|
* @returns {(Reaction| CustomEmoji)[]}
|
||||||
*/
|
*/
|
||||||
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => {
|
handleReactions = async ({reactions, prepareRecordsOnly}: HandleReactionsArgs) => {
|
||||||
|
let batchRecords: (Reaction| CustomEmoji)[] = [];
|
||||||
|
|
||||||
if (!reactions.length) {
|
if (!reactions.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "reactions" array has been passed to the handleReactions method',
|
'An empty "reactions" array has been passed to the handleReactions method',
|
||||||
|
|
@ -137,8 +139,6 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||||
rawReactions: rawValues,
|
rawReactions: rawValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
let batchRecords: Model[] = [];
|
|
||||||
|
|
||||||
if (createReactions.length) {
|
if (createReactions.length) {
|
||||||
// Prepares record for model Reactions
|
// Prepares record for model Reactions
|
||||||
const reactionsRecords = (await this.prepareRecords({
|
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 {RawUser[]} usersArgs.users
|
||||||
* @param {boolean} usersArgs.prepareRecordsOnly
|
* @param {boolean} usersArgs.prepareRecordsOnly
|
||||||
* @throws DataOperatorException
|
* @throws DataOperatorException
|
||||||
* @returns {User[] | boolean}
|
* @returns {User[]}
|
||||||
*/
|
*/
|
||||||
handleUsers = async ({users, prepareRecordsOnly = true}: HandleUsersArgs) => {
|
handleUsers = async ({users, prepareRecordsOnly = true}: HandleUsersArgs) => {
|
||||||
|
let records: User[] = [];
|
||||||
|
|
||||||
if (!users.length) {
|
if (!users.length) {
|
||||||
throw new DataOperatorException(
|
throw new DataOperatorException(
|
||||||
'An empty "users" array has been passed to the handleUsers method',
|
'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 rawValues = getUniqueRawsBy({raws: users, key: 'id'});
|
||||||
|
|
||||||
const records = await this.handleEntityRecords({
|
records = await this.handleEntityRecords({
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
findMatchingRecordBy: isRecordUserEqualToRaw,
|
findMatchingRecordBy: isRecordUserEqualToRaw,
|
||||||
operator: prepareUserRecord,
|
operator: prepareUserRecord,
|
||||||
|
|
@ -203,7 +205,7 @@ const UserHandler = (superclass: any) => class extends superclass {
|
||||||
prepareRecordsOnly,
|
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 PostHandler, {PostHandlerMix} from '@database/operator/handlers/post';
|
||||||
import TeamHandler, {TeamHandlerMix} from '@database/operator/handlers/team';
|
import TeamHandler, {TeamHandlerMix} from '@database/operator/handlers/team';
|
||||||
import UserHandler, {UserHandlerMix} from '@database/operator/handlers/user';
|
import UserHandler, {UserHandlerMix} from '@database/operator/handlers/user';
|
||||||
|
import {DatabaseInstance} from '@typings/database/database';
|
||||||
import mix from '@utils/mix';
|
import mix from '@utils/mix';
|
||||||
|
|
||||||
interface Operator extends BaseHandlerMix, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
|
interface Operator extends BaseHandlerMix, PostHandlerMix, UserHandlerMix, GroupHandlerMix, ChannelHandlerMix, TeamHandlerMix {}
|
||||||
|
|
||||||
class Operator extends mix(BaseHandler).with(
|
class Operator extends mix(BaseHandler).with(PostHandler, UserHandler, GroupHandler, ChannelHandler, TeamHandler) {
|
||||||
PostHandler,
|
database: DatabaseInstance;
|
||||||
UserHandler,
|
|
||||||
GroupHandler,
|
|
||||||
ChannelHandler,
|
|
||||||
TeamHandler,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
import {
|
import {
|
||||||
prepareAppRecord,
|
prepareAppRecord,
|
||||||
prepareCustomEmojiRecord,
|
prepareCustomEmojiRecord,
|
||||||
|
|
@ -11,14 +12,21 @@ import {
|
||||||
prepareTermsOfServiceRecord,
|
prepareTermsOfServiceRecord,
|
||||||
} from '@database/operator/prepareRecords/general';
|
} from '@database/operator/prepareRecords/general';
|
||||||
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
import {createTestConnection} from '@database/operator/utils/create_test_connection';
|
||||||
import DatabaseManager from '@database/manager';
|
|
||||||
import {OperationType} from '@typings/database/enums';
|
import {OperationType} from '@typings/database/enums';
|
||||||
|
|
||||||
|
jest.mock('@database/manager');
|
||||||
|
|
||||||
describe('*** Isolated Prepare Records Test ***', () => {
|
describe('*** Isolated Prepare Records Test ***', () => {
|
||||||
|
let databaseManagerClient: DatabaseManager;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
databaseManagerClient = new DatabaseManager();
|
||||||
|
});
|
||||||
|
|
||||||
it('=> prepareAppRecord: should return an array of type App', async () => {
|
it('=> prepareAppRecord: should return an array of type App', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const database = await DatabaseManager.getDefaultDatabase();
|
const database = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(database).toBeTruthy();
|
expect(database).toBeTruthy();
|
||||||
|
|
||||||
const preparedRecords = await prepareAppRecord({
|
const preparedRecords = await prepareAppRecord({
|
||||||
|
|
@ -41,7 +49,7 @@ describe('*** Isolated Prepare Records Test ***', () => {
|
||||||
it('=> prepareGlobalRecord: should return an array of type Global', async () => {
|
it('=> prepareGlobalRecord: should return an array of type Global', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const database = await DatabaseManager.getDefaultDatabase();
|
const database = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(database).toBeTruthy();
|
expect(database).toBeTruthy();
|
||||||
|
|
||||||
const preparedRecords = await prepareGlobalRecord({
|
const preparedRecords = await prepareGlobalRecord({
|
||||||
|
|
@ -60,7 +68,7 @@ describe('*** Isolated Prepare Records Test ***', () => {
|
||||||
it('=> prepareServersRecord: should return an array of type Servers', async () => {
|
it('=> prepareServersRecord: should return an array of type Servers', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
const database = await DatabaseManager.getDefaultDatabase();
|
const database = await databaseManagerClient.getDefaultDatabase();
|
||||||
expect(database).toBeTruthy();
|
expect(database).toBeTruthy();
|
||||||
|
|
||||||
const preparedRecords = await prepareServersRecord({
|
const preparedRecords = await prepareServersRecord({
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ export const preparePreferenceRecord = ({action, database, value}: DataFactoryAr
|
||||||
|
|
||||||
// id of preference comes from server response
|
// id of preference comes from server response
|
||||||
const generator = (preference: Preference) => {
|
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.category = raw.category;
|
||||||
preference.name = raw.name;
|
preference.name = raw.name;
|
||||||
preference.userId = raw.user_id;
|
preference.userId = raw.user_id;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ jest.mock('@database/manager');
|
||||||
|
|
||||||
export const createTestConnection = async ({databaseName = 'db_name', setActive = false}) => {
|
export const createTestConnection = async ({databaseName = 'db_name', setActive = false}) => {
|
||||||
const serverUrl = 'https://appv2.mattermost.com';
|
const serverUrl = 'https://appv2.mattermost.com';
|
||||||
const database = await DatabaseManager.createDatabaseConnection({
|
const databaseClient = new DatabaseManager();
|
||||||
|
const database = await databaseClient.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
@ -20,10 +21,7 @@ export const createTestConnection = async ({databaseName = 'db_name', setActive
|
||||||
});
|
});
|
||||||
|
|
||||||
if (setActive) {
|
if (setActive) {
|
||||||
await DatabaseManager.setActiveServerDatabase({
|
await databaseClient.setActiveServerDatabase(serverUrl);
|
||||||
displayName: databaseName,
|
|
||||||
serverUrl,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return database;
|
return database;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import User from '@typings/database/user';
|
||||||
const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
|
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.
|
* 100% sure that the records are actually different from what we already have in the database.
|
||||||
* @param {IdenticalRecordArgs} identicalRecord
|
* @param {IdenticalRecordArgs} identicalRecord
|
||||||
* @param {string} identicalRecord.tableName
|
* @param {string} identicalRecord.tableName
|
||||||
|
|
@ -31,16 +31,26 @@ const {CHANNEL, POST, SLASH_COMMAND, TEAM, USER} = MM_TABLES.SERVER;
|
||||||
* @param {Model} identicalRecord.existingRecord
|
* @param {Model} identicalRecord.existingRecord
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
export const hasSimilarUpdateAt = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
|
export const getValidRecordsForUpdate = ({tableName, newValue, existingRecord}: IdenticalRecordArgs) => {
|
||||||
const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER];
|
const guardTables = [CHANNEL, POST, SLASH_COMMAND, TEAM, USER];
|
||||||
|
|
||||||
if (guardTables.includes(tableName)) {
|
if (guardTables.includes(tableName)) {
|
||||||
type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel;
|
type Raw = RawPost | RawUser | RawTeam | RawSlashCommand | RawChannel;
|
||||||
type ExistingRecord = Post | User | Team | SlashCommand | Channel;
|
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 {RetrieveRecordsArgs} records
|
||||||
* @param {Database} records.database
|
* @param {Database} records.database
|
||||||
* @param {string} records.tableName
|
* @param {string} records.tableName
|
||||||
* @param {any} records.condition
|
* @param {Clause} records.condition
|
||||||
* @returns {Promise<Model[]>}
|
* @returns {Promise<Model[]>}
|
||||||
*/
|
*/
|
||||||
export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => {
|
export const retrieveRecords = ({database, tableName, condition}: RetrieveRecordsArgs) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {createPostsChain, sanitizePosts} from '@database/operator/utils/post';
|
||||||
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
import {sanitizeReactions} from '@database/operator/utils/reaction';
|
||||||
import DatabaseManager from '@database/manager';
|
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 () => {
|
it('=> sanitizeReactions: should triage between reactions that needs creation/deletion and emojis to be created', async () => {
|
||||||
const dbName = 'server_schema_connection';
|
const dbName = 'server_schema_connection';
|
||||||
const serverUrl = 'https://appv2.mattermost.com';
|
const serverUrl = 'https://appv2.mattermost.com';
|
||||||
const database = await DatabaseManager.createDatabaseConnection({
|
const databaseManagerClient = new DatabaseManager();
|
||||||
|
const database = await databaseManagerClient.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
@ -80,13 +81,12 @@ describe('DataOperator: Utils tests', () => {
|
||||||
serverUrl,
|
serverUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await DatabaseManager.setActiveServerDatabase({
|
await databaseManagerClient.setActiveServerDatabase(serverUrl);
|
||||||
displayName: dbName,
|
|
||||||
serverUrl,
|
const operatorClient = new Operator(database!);
|
||||||
});
|
|
||||||
|
|
||||||
// we commit one Reaction to our database
|
// we commit one Reaction to our database
|
||||||
const prepareRecords = await DataOperator.handleReactions({
|
const prepareRecords = await operatorClient.handleReactions({
|
||||||
reactions: [
|
reactions: [
|
||||||
{
|
{
|
||||||
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
|
user_id: 'beqkgo4wzbn98kjzjgc1p5n91o',
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,19 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 DatabaseManager from '@database/manager';
|
||||||
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
|
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
|
||||||
|
|
||||||
export const createDataOperator = async (serverUrl: string) => {
|
export const createDataOperator = async (serverUrl: string) => {
|
||||||
// Retrieves the connection matching serverUrl
|
const databaseManagerClient = new DatabaseManager();
|
||||||
const connections = await DatabaseManager.retrieveDatabaseInstances([
|
|
||||||
serverUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (connections?.length) {
|
const connection = await databaseManagerClient.getDatabaseConnection({serverUrl, setAsActiveDatabase: false});
|
||||||
// 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);
|
|
||||||
|
|
||||||
|
if (connection) {
|
||||||
|
const operator = new Operator(connection);
|
||||||
return operator;
|
return operator;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DatabaseConnectionException(
|
throw new DatabaseConnectionException(
|
||||||
`No database has been registered with this url: ${serverUrl}`,
|
`No database has been registered with this url: ${serverUrl}`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {createDataOperator} from '@database/operator/wrapper/index';
|
import {createDataOperator} from '@database/operator/wrapper/index';
|
||||||
import DatabaseConnectionException from '@database/exceptions/database_connection_exception';
|
|
||||||
import {DatabaseType} from '@typings/database/enums';
|
import {DatabaseType} from '@typings/database/enums';
|
||||||
|
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
|
|
@ -11,13 +10,19 @@ jest.mock('@database/manager');
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
describe('*** DataOperator Wrapper ***', () => {
|
describe('*** DataOperator Wrapper ***', () => {
|
||||||
|
let databaseManagerClient: DatabaseManager;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
databaseManagerClient = new DatabaseManager();
|
||||||
|
});
|
||||||
|
|
||||||
it('=> wrapper should return an instance of DataOperator ', async () => {
|
it('=> wrapper should return an instance of DataOperator ', async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const serverUrl = 'https://wrapper.mattermost.com';
|
const serverUrl = 'https://wrapper.mattermost.com';
|
||||||
|
|
||||||
// first we create the connection and save it into default database
|
// first we create the connection and save it into default database
|
||||||
await DatabaseManager.createDatabaseConnection({
|
await databaseManagerClient.createDatabaseConnection({
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
dbName: 'community mattermost',
|
dbName: 'community mattermost',
|
||||||
|
|
@ -32,29 +37,6 @@ describe('*** DataOperator Wrapper ***', () => {
|
||||||
expect(dataOperator).toBeTruthy();
|
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 () => {
|
it('=> wrapper to handlePosts [OTHER DATABASE]: should write to Post and its sub-child entities', async () => {
|
||||||
expect.assertions(12);
|
expect.assertions(12);
|
||||||
|
|
||||||
|
|
@ -212,7 +194,7 @@ describe('*** DataOperator Wrapper ***', () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
// create connection to other server in default db
|
// create connection to other server in default db
|
||||||
await DatabaseManager.createDatabaseConnection({
|
await databaseManagerClient.createDatabaseConnection({
|
||||||
shouldAddToDefaultDatabase: true,
|
shouldAddToDefaultDatabase: true,
|
||||||
configs: {
|
configs: {
|
||||||
actionsEnabled: true,
|
actionsEnabled: true,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ export default tableSchema({
|
||||||
{name: 'roles', type: 'string'},
|
{name: 'roles', type: 'string'},
|
||||||
{name: 'status', type: 'string'},
|
{name: 'status', type: 'string'},
|
||||||
{name: 'timezone', type: 'string'},
|
{name: 'timezone', type: 'string'},
|
||||||
{name: 'user_id', type: 'string'},
|
|
||||||
{name: 'username', type: 'string'},
|
{name: 'username', type: 'string'},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -492,7 +492,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||||
roles: {name: 'roles', type: 'string'},
|
roles: {name: 'roles', type: 'string'},
|
||||||
status: {name: 'status', type: 'string'},
|
status: {name: 'status', type: 'string'},
|
||||||
timezone: {name: 'timezone', type: 'string'},
|
timezone: {name: 'timezone', type: 'string'},
|
||||||
user_id: {name: 'user_id', type: 'string'},
|
|
||||||
username: {name: 'username', type: 'string'},
|
username: {name: 'username', type: 'string'},
|
||||||
},
|
},
|
||||||
columnArray: [
|
columnArray: [
|
||||||
|
|
@ -513,7 +512,6 @@ describe('*** Test schema for SERVER database ***', () => {
|
||||||
{name: 'roles', type: 'string'},
|
{name: 'roles', type: 'string'},
|
||||||
{name: 'status', type: 'string'},
|
{name: 'status', type: 'string'},
|
||||||
{name: 'timezone', type: 'string'},
|
{name: 'timezone', type: 'string'},
|
||||||
{name: 'user_id', type: 'string'},
|
|
||||||
{name: 'username', type: 'string'},
|
{name: 'username', type: 'string'},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,82 +2,172 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
|
import {getLocales} from 'react-native-localize';
|
||||||
|
|
||||||
import en from '@assets/i18n/en.json';
|
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 {
|
try {
|
||||||
let translations;
|
let translations;
|
||||||
let momentData;
|
let momentData;
|
||||||
|
|
||||||
switch (locale) {
|
switch (locale) {
|
||||||
case 'de':
|
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');
|
translations = require('@assets/i18n/de.json');
|
||||||
momentData = require('moment/locale/de');
|
momentData = require('moment/locale/de');
|
||||||
break;
|
break;
|
||||||
case 'es':
|
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');
|
translations = require('@assets/i18n/es.json');
|
||||||
momentData = require('moment/locale/es');
|
momentData = require('moment/locale/es');
|
||||||
break;
|
break;
|
||||||
case 'fr':
|
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');
|
translations = require('@assets/i18n/fr.json');
|
||||||
momentData = require('moment/locale/fr');
|
momentData = require('moment/locale/fr');
|
||||||
break;
|
break;
|
||||||
case 'it':
|
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');
|
translations = require('@assets/i18n/it.json');
|
||||||
momentData = require('moment/locale/it');
|
momentData = require('moment/locale/it');
|
||||||
break;
|
break;
|
||||||
case 'ja':
|
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');
|
translations = require('@assets/i18n/ja.json');
|
||||||
momentData = require('moment/locale/ja');
|
momentData = require('moment/locale/ja');
|
||||||
break;
|
break;
|
||||||
case 'ko':
|
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');
|
translations = require('@assets/i18n/ko.json');
|
||||||
momentData = require('moment/locale/ko');
|
momentData = require('moment/locale/ko');
|
||||||
break;
|
break;
|
||||||
case 'nl':
|
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');
|
translations = require('@assets/i18n/nl.json');
|
||||||
momentData = require('moment/locale/nl');
|
momentData = require('moment/locale/nl');
|
||||||
break;
|
break;
|
||||||
case 'pl':
|
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');
|
translations = require('@assets/i18n/pl.json');
|
||||||
momentData = require('moment/locale/pl');
|
momentData = require('moment/locale/pl');
|
||||||
break;
|
break;
|
||||||
case 'pt-BR':
|
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');
|
translations = require('@assets/i18n/pt-BR.json');
|
||||||
momentData = require('moment/locale/pt-br');
|
momentData = require('moment/locale/pt-br');
|
||||||
break;
|
break;
|
||||||
case 'ro':
|
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');
|
translations = require('@assets/i18n/ro.json');
|
||||||
momentData = require('moment/locale/ro');
|
momentData = require('moment/locale/ro');
|
||||||
break;
|
break;
|
||||||
case 'ru':
|
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');
|
translations = require('@assets/i18n/ru.json');
|
||||||
momentData = require('moment/locale/ru');
|
momentData = require('moment/locale/ru');
|
||||||
break;
|
break;
|
||||||
case 'tr':
|
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');
|
translations = require('@assets/i18n/tr.json');
|
||||||
momentData = require('moment/locale/tr');
|
momentData = require('moment/locale/tr');
|
||||||
break;
|
break;
|
||||||
case 'uk':
|
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');
|
translations = require('@assets/i18n/uk.json');
|
||||||
momentData = require('moment/locale/uk');
|
momentData = require('moment/locale/uk');
|
||||||
break;
|
break;
|
||||||
case 'zh-CN':
|
case 'zh-CN':
|
||||||
|
loadChinesePolyfills();
|
||||||
translations = require('@assets/i18n/zh-CN.json');
|
translations = require('@assets/i18n/zh-CN.json');
|
||||||
momentData = require('moment/locale/zh-cn');
|
momentData = require('moment/locale/zh-cn');
|
||||||
break;
|
break;
|
||||||
case 'zh-TW':
|
case 'zh-TW':
|
||||||
|
loadChinesePolyfills();
|
||||||
translations = require('@assets/i18n/zh-TW.json');
|
translations = require('@assets/i18n/zh-TW.json');
|
||||||
momentData = require('moment/locale/zh-tw');
|
momentData = require('moment/locale/zh-tw');
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
translations = en;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (momentData) {
|
if (momentData && locale) {
|
||||||
moment.updateLocale(locale.toLowerCase(), momentData);
|
moment.updateLocale(locale.toLowerCase(), momentData);
|
||||||
} else {
|
} else {
|
||||||
resetMomentLocale();
|
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() {
|
export function resetMomentLocale() {
|
||||||
moment.locale(DEFAULT_LOCALE);
|
moment.locale(DEFAULT_LOCALE);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTranslations(locale: string) {
|
export function getTranslations(locale?: string) {
|
||||||
return loadTranslation(locale);
|
return loadTranslation(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ const isSystemAdmin = (roles: string) => {
|
||||||
|
|
||||||
const clientMap: Record<string, Analytics> = {};
|
const clientMap: Record<string, Analytics> = {};
|
||||||
|
|
||||||
class Analytics {
|
export class Analytics {
|
||||||
analytics: RudderClient | null = null;
|
analytics: RudderClient | null = null;
|
||||||
context: any;
|
context: any;
|
||||||
diagnosticId: string | undefined;
|
diagnosticId: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,112 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Platform} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-community/async-storage';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
import * as KeyChain from 'react-native-keychain';
|
import * as KeyChain from 'react-native-keychain';
|
||||||
|
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
import * as analytics from '@init/analytics';
|
import * as analytics from '@init/analytics';
|
||||||
import emmProvider from '@init/emm_provider';
|
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
|
||||||
import {getCSRFFromCookie} from '@utils/security';
|
import {getCSRFFromCookie} from '@utils/security';
|
||||||
|
|
||||||
|
import type {ServerCredentials} from '@typings/credentials';
|
||||||
|
|
||||||
const ASYNC_STORAGE_CURRENT_SERVER_KEY = '@currentServerUrl';
|
const ASYNC_STORAGE_CURRENT_SERVER_KEY = '@currentServerUrl';
|
||||||
|
|
||||||
export const getCurrentServerUrl = async () => {
|
// TODO: This function is only necessary to support pre-Gekidou
|
||||||
// TODO: Use default database to retrieve the current server url
|
// versions as the active server URL may be stored in AsyncStorage.
|
||||||
// and fallback to AsyncStorage if needed
|
// 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;
|
return serverUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAppCredentials = (deviceToken: string, currentUserId: string, token: string, url: string) => {
|
export const setServerCredentials = (serverUrl: string, userId: string, token: string) => {
|
||||||
if (!currentUserId) {
|
if (!(serverUrl && userId && token)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only save to keychain if the url and token are set
|
try {
|
||||||
if (url && token) {
|
let accessGroup;
|
||||||
try {
|
if (Platform.OS === 'ios') {
|
||||||
const username = `${deviceToken}, ${currentUserId}`;
|
const appGroup = getIOSAppGroupDetails();
|
||||||
const accessGroup = emmProvider.getAppGroupIdentifier();
|
accessGroup = appGroup.appGroupIdentifier;
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 () => {
|
export const getActiveServerCredentials = async () => {
|
||||||
const serverUrl = await getCurrentServerUrl();
|
const serverUrl = await getActiveServerUrl();
|
||||||
|
if (serverUrl) {
|
||||||
|
return getServerCredentials(serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return getInternetCredentials(serverUrl || '');
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAppCredentials = async () => {
|
export const removeServerCredentials = async (serverUrl: string) => {
|
||||||
const url = await getCurrentServerUrl();
|
|
||||||
|
|
||||||
// TODO: invalidate client and remove tokens
|
// TODO: invalidate client and remove tokens
|
||||||
|
|
||||||
if (url) {
|
KeyChain.resetInternetCredentials(serverUrl);
|
||||||
KeyChain.resetInternetCredentials(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
EphemeralStore.currentServerUrl = null;
|
|
||||||
AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
|
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 {
|
try {
|
||||||
const credentials = await KeyChain.getInternetCredentials(url);
|
const credentials = await KeyChain.getInternetCredentials(serverUrl);
|
||||||
|
|
||||||
if (credentials) {
|
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 token = credentials.password;
|
||||||
const [deviceToken, currentUserId] = usernameParsed;
|
|
||||||
|
|
||||||
if (token && token !== 'undefined') {
|
if (token && token !== 'undefined') {
|
||||||
EphemeralStore.deviceToken = deviceToken;
|
const analyticsClient = analytics.get(serverUrl);
|
||||||
const analyticsClient = analytics.get(url);
|
analyticsClient?.setUserId(userId);
|
||||||
analyticsClient?.setUserId(currentUserId);
|
|
||||||
|
|
||||||
const csrf = await getCSRFFromCookie(url);
|
const csrf = await getCSRFFromCookie(serverUrl);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('CSRF', csrf);
|
console.log('CSRF', csrf);
|
||||||
|
|
||||||
// TODO: Create client and set token / 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) {
|
} catch (e) {
|
||||||
return null;
|
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 {Navigation} from '@constants';
|
||||||
import {DEFAULT_LOCALE, getTranslations, resetMomentLocale, t} from '@i18n';
|
import {DEFAULT_LOCALE, getTranslations, resetMomentLocale, t} from '@i18n';
|
||||||
import * as analytics from '@init/analytics';
|
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 PushNotifications from '@init/push_notifications';
|
||||||
|
import {LaunchType} from '@typings/launch';
|
||||||
import {deleteFileCache} from '@utils/file';
|
import {deleteFileCache} from '@utils/file';
|
||||||
|
|
||||||
type LinkingCallbackArg = {url: string};
|
type LinkingCallbackArg = {url: string};
|
||||||
|
|
||||||
class GlobalEventHandler {
|
class GlobalEventHandler {
|
||||||
launchApp: launchAppFunc | undefined;
|
|
||||||
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
|
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -29,9 +30,7 @@ class GlobalEventHandler {
|
||||||
Linking.addEventListener('url', this.onDeepLink);
|
Linking.addEventListener('url', this.onDeepLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
configure = (opts: GlobalEventHandlerOpts) => {
|
init = () => {
|
||||||
this.launchApp = opts.launchApp;
|
|
||||||
|
|
||||||
this.JavascriptAndNativeErrorHandler = require('@utils/error_handling').default;
|
this.JavascriptAndNativeErrorHandler = require('@utils/error_handling').default;
|
||||||
this.JavascriptAndNativeErrorHandler?.initializeErrorHandling();
|
this.JavascriptAndNativeErrorHandler?.initializeErrorHandling();
|
||||||
};
|
};
|
||||||
|
|
@ -48,11 +47,9 @@ class GlobalEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeepLink = (event: LinkingCallbackArg) => {
|
onDeepLink = (event: LinkingCallbackArg) => {
|
||||||
const {url} = event;
|
if (event.url) {
|
||||||
if (url) {
|
const props = getLaunchPropsFromDeepLink(event.url);
|
||||||
// TODO: Handle deeplink
|
relaunchApp(props);
|
||||||
// if server is not added go to add new server screen
|
|
||||||
// if server is added but no credentials found take to server login screen
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -125,8 +122,9 @@ class GlobalEventHandler {
|
||||||
analytics.invalidate(serverUrl);
|
analytics.invalidate(serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove credentials and files for the server
|
removeServerCredentials(serverUrl);
|
||||||
removeAppCredentials();
|
|
||||||
|
// TODO: remove files for the server
|
||||||
deleteFileCache();
|
deleteFileCache();
|
||||||
PushNotifications.clearNotifications();
|
PushNotifications.clearNotifications();
|
||||||
|
|
||||||
|
|
@ -136,11 +134,7 @@ class GlobalEventHandler {
|
||||||
|
|
||||||
await this.clearCookiesAndWebData();
|
await this.clearCookiesAndWebData();
|
||||||
|
|
||||||
if (this.launchApp) {
|
relaunchApp({launchType: LaunchType.Normal});
|
||||||
this.launchApp(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove the database
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onServerConfigChanged = (serverUrl: string, config: ClientConfig) => {
|
onServerConfigChanged = (serverUrl: string, config: ClientConfig) => {
|
||||||
|
|
@ -178,7 +172,7 @@ class GlobalEventHandler {
|
||||||
};
|
};
|
||||||
|
|
||||||
serverUpgradeNeeded = async (serverUrl: string) => {
|
serverUpgradeNeeded = async (serverUrl: string) => {
|
||||||
const credentials = await getAppCredentials();
|
const credentials = await getServerCredentials(serverUrl);
|
||||||
|
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
this.onLogout(serverUrl);
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// 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 {AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Platform} from 'react-native';
|
||||||
import DeviceInfo from 'react-native-device-info';
|
import DeviceInfo from 'react-native-device-info';
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,11 +16,12 @@ import {
|
||||||
Registered,
|
Registered,
|
||||||
} from 'react-native-notifications';
|
} from 'react-native-notifications';
|
||||||
|
|
||||||
import {dismissAllModals, popToRoot, showOverlay} from '@screens/navigation';
|
|
||||||
import {Device, Navigation, View} from '@constants';
|
import {Device, Navigation, View} from '@constants';
|
||||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
import Operator from '@database/operator';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import {getLaunchPropsFromNotification, relaunchApp} from '@init/launch';
|
||||||
import NativeNotifications from '@notifications';
|
import NativeNotifications from '@notifications';
|
||||||
|
import {showOverlay} from '@screens/navigation';
|
||||||
|
import {IsolatedEntities} from '@typings/database/enums';
|
||||||
|
|
||||||
const CATEGORY = 'CAN_REPLY';
|
const CATEGORY = 'CAN_REPLY';
|
||||||
const REPLY_ACTION = 'REPLY_ACTION';
|
const REPLY_ACTION = 'REPLY_ACTION';
|
||||||
|
|
@ -28,217 +31,217 @@ const NOTIFICATION_TYPE = {
|
||||||
SESSION: 'session',
|
SESSION: 'session',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//todo: Do we need Ephemeral store? Should we refactor this file ?
|
||||||
|
|
||||||
class PushNotifications {
|
class PushNotifications {
|
||||||
configured = false;
|
configured = false;
|
||||||
pushNotificationListener: EmitterSubscription | undefined;
|
pushNotificationListener: EmitterSubscription | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
Notifications.registerRemoteNotifications();
|
Notifications.registerRemoteNotifications();
|
||||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||||
AppState.addEventListener('change', this.onAppStateChange);
|
AppState.addEventListener('change', this.onAppStateChange);
|
||||||
|
}
|
||||||
|
|
||||||
this.getInitialNotification();
|
cancelAllLocalNotifications = () => {
|
||||||
}
|
Notifications.cancelAllLocalNotifications();
|
||||||
|
};
|
||||||
|
|
||||||
cancelAllLocalNotifications = () => {
|
clearNotifications = () => {
|
||||||
Notifications.cancelAllLocalNotifications();
|
this.setApplicationIconBadgeNumber(0);
|
||||||
};
|
|
||||||
|
|
||||||
clearNotifications = () => {
|
// TODO: Only cancel the local notifications that belong to this server
|
||||||
this.setApplicationIconBadgeNumber(0);
|
this.cancelAllLocalNotifications();
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Only cancel the local notifications that belong to this server
|
clearChannelNotifications = async (channelId: string) => {
|
||||||
this.cancelAllLocalNotifications();
|
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) => {
|
// TODO: Set the badgeCount with default database mention count aggregate
|
||||||
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
|
for (const notification of notifications) {
|
||||||
|
if (notification.channel_id === channelId) {
|
||||||
|
ids.push(notification.identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < notifications.length; i++) {
|
if (ids.length) {
|
||||||
const notification = notifications[i];
|
NativeNotifications.removeDeliveredNotifications(ids);
|
||||||
if (notification.channel_id === channelId) {
|
}
|
||||||
ids.push(notification.identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.length) {
|
this.setApplicationIconBadgeNumber(badgeCount);
|
||||||
NativeNotifications.removeDeliveredNotifications(ids);
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.setApplicationIconBadgeNumber(badgeCount);
|
createReplyCategory = () => {
|
||||||
}
|
const locale = DEFAULT_LOCALE; // TODO: Get the current user locale to replace the old getCurrentLocale(state);
|
||||||
};
|
|
||||||
|
|
||||||
createReplyCategory = () => {
|
const replyTitle = getLocalizedMessage(locale, t('mobile.push_notification_reply.title'));
|
||||||
const locale = DEFAULT_LOCALE; // TODO: Get the current user locale to replace the old getCurrentLocale(state);
|
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'));
|
handleNotification = async (notification: NotificationWithData) => {
|
||||||
const replyButton = getLocalizedMessage(locale, t('mobile.push_notification_reply.button'));
|
const {payload, foreground, userInteraction} = notification;
|
||||||
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]);
|
|
||||||
};
|
|
||||||
|
|
||||||
getInitialNotification = async () => {
|
if (payload) {
|
||||||
const notification: NotificationWithData | undefined = await Notifications.getInitialNotification();
|
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) {
|
if (foreground) {
|
||||||
EphemeralStore.setStartFromNotification(true);
|
// Show the in-app notification
|
||||||
notification.userInteraction = true;
|
} else if (userInteraction && !payload.userInfo?.local) {
|
||||||
this.handleNotification(notification);
|
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) => {
|
// Logout the user from the server that matches the notification
|
||||||
const {payload, foreground, userInteraction} = notification;
|
|
||||||
|
|
||||||
if (payload) {
|
break;
|
||||||
switch (payload.type) {
|
}
|
||||||
case NOTIFICATION_TYPE.CLEAR:
|
}
|
||||||
// Mark the channel as read
|
};
|
||||||
break;
|
|
||||||
case NOTIFICATION_TYPE.MESSAGE:
|
|
||||||
// fetch the posts for the channel
|
|
||||||
|
|
||||||
if (foreground) {
|
handleInAppNotification = async (notification: NotificationWithData) => {
|
||||||
// Show the in-app notification
|
const {payload} = notification;
|
||||||
} else if (userInteraction && !payload.userInfo?.local) {
|
|
||||||
// Swith to the server / team / channel that matches the notification
|
|
||||||
|
|
||||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
// TODO: Get current channel from the database
|
||||||
if (componentId) {
|
const currentChannelId = '';
|
||||||
// Emit events to close the sidebars
|
|
||||||
|
|
||||||
await dismissAllModals();
|
if (payload?.channel_id !== currentChannelId) {
|
||||||
await popToRoot();
|
const screen = 'Notification';
|
||||||
}
|
const passProps = {
|
||||||
}
|
notification,
|
||||||
break;
|
};
|
||||||
case NOTIFICATION_TYPE.SESSION:
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Session expired 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) => {
|
onAppStateChange = (appState: AppStateStatus) => {
|
||||||
const {payload} = notification;
|
const isActive = appState === 'active';
|
||||||
|
const isBackground = appState === 'background';
|
||||||
|
|
||||||
// TODO: Get current channel from the database
|
if (isActive) {
|
||||||
const currentChannelId = '';
|
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) {
|
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
|
||||||
const screen = 'Notification';
|
notification.userInteraction = true;
|
||||||
const passProps = {
|
this.handleNotification(notification);
|
||||||
notification,
|
completion();
|
||||||
};
|
};
|
||||||
|
|
||||||
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
|
onNotificationReceivedBackground = (notification: NotificationWithData, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
||||||
showOverlay(screen, passProps);
|
this.handleNotification(notification);
|
||||||
}
|
completion(NotificationBackgroundFetchResult.NO_DATA);
|
||||||
}
|
};
|
||||||
|
|
||||||
localNotification = (notification: Notification) => {
|
onNotificationReceivedForeground = (notification: NotificationWithData, completion: (response: NotificationCompletion) => void) => {
|
||||||
Notifications.postLocalNotification(notification);
|
notification.foreground = AppState.currentState === 'active';
|
||||||
};
|
completion({alert: false, sound: true, badge: true});
|
||||||
|
this.handleNotification(notification);
|
||||||
|
};
|
||||||
|
|
||||||
onAppStateChange = (appState: AppStateStatus) => {
|
onRemoteNotificationsRegistered = async (event: Registered) => {
|
||||||
const isActive = appState === 'active';
|
if (!this.configured) {
|
||||||
const isBackground = appState === 'background';
|
this.configured = true;
|
||||||
|
const {deviceToken} = event;
|
||||||
|
let prefix;
|
||||||
|
|
||||||
if (isActive) {
|
if (Platform.OS === 'ios') {
|
||||||
if (!this.pushNotificationListener) {
|
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
||||||
this.pushNotificationListener = DeviceEventEmitter.addListener(View.NOTIFICATION_IN_APP, this.handleInAppNotification);
|
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
||||||
}
|
prefix = `${prefix}beta`;
|
||||||
} else if (isBackground) {
|
}
|
||||||
this.pushNotificationListener?.remove();
|
} else {
|
||||||
this.pushNotificationListener = undefined;
|
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
|
const {activeServerDatabase, error} = await getActiveServerDatabase();
|
||||||
notification.userInteraction = true;
|
|
||||||
this.handleNotification(notification);
|
|
||||||
completion();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNotificationReceivedBackground = (notification: NotificationWithData, completion: (response: NotificationBackgroundFetchResult) => void) => {
|
if (!activeServerDatabase) {
|
||||||
this.handleNotification(notification);
|
return {error};
|
||||||
completion(NotificationBackgroundFetchResult.NO_DATA);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
onNotificationReceivedForeground = (notification: NotificationWithData, completion: (response: NotificationCompletion) => void) => {
|
const operator = new Operator(activeServerDatabase);
|
||||||
notification.foreground = AppState.currentState === 'active';
|
|
||||||
completion({alert: false, sound: true, badge: true});
|
|
||||||
this.handleNotification(notification);
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoteNotificationsRegistered = (event: Registered) => {
|
operator.handleIsolatedEntity({
|
||||||
if (!this.configured) {
|
tableName: IsolatedEntities.GLOBAL,
|
||||||
this.configured = true;
|
values: [{name: 'deviceToken', value: `${prefix}:${deviceToken}`}],
|
||||||
const {deviceToken} = event;
|
prepareRecordsOnly: false,
|
||||||
let prefix;
|
});
|
||||||
|
|
||||||
if (Platform.OS === 'ios') {
|
// Store the device token in the default database
|
||||||
prefix = Device.PUSH_NOTIFY_APPLE_REACT_NATIVE;
|
this.requestNotificationReplyPermissions();
|
||||||
if (DeviceInfo.getBundleId().includes('rnbeta')) {
|
}
|
||||||
prefix = `${prefix}beta`;
|
return null;
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
EphemeralStore.deviceToken = `${prefix}:${deviceToken}`;
|
requestNotificationReplyPermissions = () => {
|
||||||
|
if (Platform.OS === 'ios') {
|
||||||
|
const replyCategory = this.createReplyCategory();
|
||||||
|
Notifications.setCategories([replyCategory]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Store the device token in the default database
|
setApplicationIconBadgeNumber = (value: number) => {
|
||||||
this.requestNotificationReplyPermissions();
|
if (Platform.OS === 'ios') {
|
||||||
}
|
const count = value < 0 ? 0 : value;
|
||||||
};
|
Notifications.ios.setBadgeCount(count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
requestNotificationReplyPermissions = () => {
|
scheduleNotification = (notification: Notification) => {
|
||||||
if (Platform.OS === 'ios') {
|
if (notification.fireDate) {
|
||||||
const replyCategory = this.createReplyCategory();
|
if (Platform.OS === 'ios') {
|
||||||
Notifications.setCategories([replyCategory]);
|
notification.fireDate = new Date(notification.fireDate).toISOString();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
setApplicationIconBadgeNumber = (value: number) => {
|
Notifications.postLocalNotification(notification);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PushNotifications();
|
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,
|
ReloadInstructions,
|
||||||
} from 'react-native/Libraries/NewAppScreen';
|
} from 'react-native/Libraries/NewAppScreen';
|
||||||
|
|
||||||
|
import type {LaunchProps} from '@typings/launch';
|
||||||
|
|
||||||
declare const global: {HermesInternal: null | {}};
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<StatusBar barStyle='dark-content'/>
|
<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.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {IntlProvider} from 'react-intl';
|
||||||
|
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
|
||||||
import {Platform, StyleProp, ViewStyle} from 'react-native';
|
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 {gestureHandlerRootHOC} from 'react-native-gesture-handler';
|
||||||
|
|
||||||
|
import {withManagedConfig} from '@mattermost/react-native-emm';
|
||||||
|
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
|
|
||||||
// TODO: Remove this and uncomment screens as they get added
|
// TODO: Remove this and uncomment screens as they get added
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewStyle>) => {
|
const withGestures = (screen: NavigationFunctionComponent, styles: StyleProp<ViewStyle>) => {
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
return gestureHandlerRootHOC(screen, styles);
|
return gestureHandlerRootHOC(screen, styles);
|
||||||
}
|
}
|
||||||
|
|
@ -21,10 +23,23 @@ const withGestures = (screen: React.ComponentType<any>, styles: StyleProp<ViewSt
|
||||||
return screen;
|
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) => {
|
Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
// let screen: any;
|
let screen: any|undefined;
|
||||||
// let extraStyles: StyleProp<ViewStyle>;
|
let extraStyles: StyleProp<ViewStyle>;
|
||||||
// switch (screenName) {
|
switch (screenName) {
|
||||||
// case 'About':
|
// case 'About':
|
||||||
// screen = require('@screens/about').default;
|
// screen = require('@screens/about').default;
|
||||||
// break;
|
// break;
|
||||||
|
|
@ -79,30 +94,30 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
// case 'FlaggedPosts':
|
// case 'FlaggedPosts':
|
||||||
// screen = require('@screens/flagged_posts').default;
|
// screen = require('@screens/flagged_posts').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'ForgotPassword':
|
case 'ForgotPassword':
|
||||||
// screen = require('@screens/forgot_password').default;
|
screen = require('@screens/forgot_password').default;
|
||||||
// break;
|
break;
|
||||||
// case 'Gallery':
|
// case 'Gallery':
|
||||||
// screen = require('@screens/gallery').default;
|
// screen = require('@screens/gallery').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'InteractiveDialog':
|
// case 'InteractiveDialog':
|
||||||
// screen = require('@screens/interactive_dialog').default;
|
// screen = require('@screens/interactive_dialog').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'Login':
|
case 'Login':
|
||||||
// screen = require('@screens/login').default;
|
screen = require('@screens/login').default;
|
||||||
// break;
|
break;
|
||||||
// case 'LoginOptions':
|
case 'LoginOptions':
|
||||||
// screen = require('@screens/login_options').default;
|
screen = require('@screens/login_options').default;
|
||||||
// break;
|
break;
|
||||||
// case 'LongPost':
|
// case 'LongPost':
|
||||||
// screen = require('@screens/long_post').default;
|
// screen = require('@screens/long_post').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'MainSidebar':
|
// case 'MainSidebar':
|
||||||
// screen = require('app/components/sidebars/main').default;
|
// screen = require('app/components/sidebars/main').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'MFA':
|
case 'MFA':
|
||||||
// screen = require('@screens/mfa').default;
|
screen = require('@screens/mfa').default;
|
||||||
// break;
|
break;
|
||||||
// case 'MoreChannels':
|
// case 'MoreChannels':
|
||||||
// screen = require('@screens/more_channels').default;
|
// screen = require('@screens/more_channels').default;
|
||||||
// break;
|
// break;
|
||||||
|
|
@ -170,9 +185,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
// case 'SidebarSettings':
|
// case 'SidebarSettings':
|
||||||
// screen = require('@screens/settings/sidebar').default;
|
// screen = require('@screens/settings/sidebar').default;
|
||||||
// break;
|
// break;
|
||||||
// case 'SSO':
|
case 'SSO':
|
||||||
// screen = require('@screens/sso').default;
|
screen = require('@screens/sso').default;
|
||||||
// break;
|
break;
|
||||||
// case 'Table':
|
// case 'Table':
|
||||||
// screen = require('@screens/table').default;
|
// screen = require('@screens/table').default;
|
||||||
// break;
|
// break;
|
||||||
|
|
@ -191,11 +206,11 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||||
// case 'UserProfile':
|
// case 'UserProfile':
|
||||||
// screen = require('@screens/user_profile').default;
|
// screen = require('@screens/user_profile').default;
|
||||||
// break;
|
// break;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (screen) {
|
if (screen) {
|
||||||
// Navigation.registerComponent(screenName, () => withGestures(withManagedConfig(screen), extraStyles));
|
Navigation.registerComponent(screenName, () => withGestures(withIntl(withManagedConfig(screen)), extraStyles));
|
||||||
// }
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function registerScreens() {
|
export function registerScreens() {
|
||||||
|
|
@ -203,5 +218,5 @@ export function registerScreens() {
|
||||||
const serverScreen = require('@screens/server').default;
|
const serverScreen = require('@screens/server').default;
|
||||||
|
|
||||||
Navigation.registerComponent(Screens.CHANNEL, () => withManagedConfig(channelScreen));
|
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