Compare commits

..

9 Commits

Author SHA1 Message Date
Elias Nahum
97ab2f86cb Merge branch 'main' into voice-message 2023-01-24 22:57:59 +02:00
Daniel Espino García
79ee4ebb30 Merge branch 'main' into voice-message 2023-01-17 13:16:47 +01:00
Elias Nahum
0db1d556df Merge branch 'main' into voice-message 2023-01-02 12:32:38 +02:00
Elias Nahum
441e710bc1 add react-native-audio-recorder-player mp3 fork (#6681) 2022-10-13 08:37:34 -03:00
Daniel Espino García
f9b7932920 Merge branch 'gekidou' into voice-message 2022-10-13 11:41:06 +02:00
Avinash Lingaloo
97cd096934 Voice message recording UI (#6661)
* record action button

added record_action button

* wiring components

some more components

update condition

* animated sound wave

sound wave animation - nearly there

* animated microphone

animated microphone in progress

* fix wiring

fix wiring

fix wiring

* PR review fix

* voice record upload

voice record upload

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

* record action
2022-10-13 10:47:05 +02:00
Daniel Espino García
081ac80fde Improve wiring after test with the recording library (#6659) 2022-10-07 10:04:17 +02:00
Daniel Espino García
f9f0e95ce1 Remove Edit option for voice messages (#6654) 2022-10-03 11:36:55 +02:00
Daniel Espino García
9854683321 Voice Messages (#6651)
* Initial skeleton

* Cleanup and fix errors

* Minor refactoring and add config

* Fix lint

* Add selection logic to post body

* Fix naming
2022-09-21 23:06:03 +02:00
292 changed files with 6059 additions and 6856 deletions

View File

@@ -114,7 +114,6 @@ commands:
command: |
NODE_ENV=development npm ci --ignore-scripts
node node_modules/\@sentry/cli/scripts/install.js
node node_modules/react-native-webrtc/tools/downloadWebRTC.js
- save_cache:
name: Save npm cache
key: v2-npm-{{ checksum "package.json" }}-{{ arch }}

View File

@@ -91,39 +91,6 @@ A spec-compliant polyfill for Intl.RelativeTimeFormat fully tested by the offici
---
## @gorhom/bottom-sheet
This product contains '@gorhom/bottom-sheet' by Mo Gorhom.
A performant interactive bottom sheet with fully configurable options
* HOMEPAGE:
* https://github.com/gorhom/react-native-bottom-sheet
* LICENSE: MIT License
Copyright (c) 2020 Mo Gorhom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## @mattermost/compass-icons
This product contains '@mattermost/compass-icons' by Mattermost.
@@ -2824,38 +2791,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-walkthrough-tooltip
This product contains 'react-native-walkthrough-tooltip' by Jason Gaare.
React Native Walkthrough Tooltip is a fullscreen modal that highlights whichever element it wraps.
* HOMEPAGE:
* https://github.com/jasongaare/react-native-walkthrough-tooltip
* LICENSE: MIT License
Copyright (c) 2018 Jason Gaare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---

View File

@@ -112,8 +112,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 457
versionName "2.1.0"
versionCode 453
versionName "2.0.0"
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
@@ -206,10 +206,6 @@ dependencies {
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'androidx.window:window-rxjava3:1.0.0'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

View File

@@ -12,7 +12,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
@@ -41,6 +40,7 @@
android:resizeableActivity="true"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup"
>
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="android.content.APP_RESTRICTIONS"

View File

@@ -1,28 +0,0 @@
package com.mattermost.helpers
import android.graphics.Bitmap
import android.util.LruCache
class BitmapCache {
private var memoryCache: LruCache<String, Bitmap>
init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
}
fun getBitmapFromMemCache(key: String): Bitmap? {
return memoryCache.get(key)
}
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap)
}
}
}

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -27,6 +28,7 @@ import androidx.core.app.Person;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.mattermost.rnbeta.*;
import java.io.IOException;
@@ -51,11 +53,7 @@ public class CustomPushNotificationHelper {
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
private static final OkHttpClient client = new OkHttpClient();
private static final BitmapCache bitmapCache = new BitmapCache();
private static void addMessagingStyleMessages(NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
String serverUrl = bundle.getString("server_url");
@@ -77,7 +75,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -179,12 +177,12 @@ public class CustomPushNotificationHelper {
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
addNotificationExtras(notification, bundle);
setNotificationIcons(notification, bundle);
setNotificationMessagingStyle(notification, bundle);
setNotificationIcons(context, notification, bundle);
setNotificationMessagingStyle(context, notification, bundle);
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
setNotificationChannel(context, notification);
setNotificationChannel(notification, bundle);
setNotificationDeleteIntent(context, notification, bundle, notificationId);
addNotificationReplyAction(context, notification, bundle, notificationId);
@@ -256,7 +254,15 @@ public class CustomPushNotificationHelper {
return title;
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
private static int getIconResourceId(Context context, String iconName) {
final Resources res = context.getResources();
String packageName = context.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
final String senderId = "me";
final String serverUrl = bundle.getString("server_url");
@@ -269,7 +275,7 @@ public class CustomPushNotificationHelper {
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
try {
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
if (avatar != null) {
sender.setIcon(IconCompat.createWithBitmap(avatar));
}
@@ -282,7 +288,7 @@ public class CustomPushNotificationHelper {
String conversationTitle = getConversationTitle(bundle);
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
return messagingStyle;
}
@@ -309,6 +315,25 @@ public class CustomPushNotificationHelper {
return getConversationTitle(bundle);
}
public static int getSmallIconResourceId(Context context, String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private static String removeSenderNameFromMessage(String message, String senderName) {
int index = message.indexOf(senderName);
if (index == 0) {
@@ -340,15 +365,12 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (mHighImportanceChannel == null) {
createNotificationChannels(context);
}
NotificationChannel notificationChannel = mHighImportanceChannel;
notification.setChannelId(notificationChannel.getId());
}
@@ -364,8 +386,8 @@ public class CustomPushNotificationHelper {
notification.setDeleteIntent(deleteIntent);
}
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
notification.setStyle(messagingStyle);
}
@@ -378,18 +400,20 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
String serverUrl = bundle.getString("server_url");
String urlOverride = bundle.getString("override_icon_url");
notification.setSmallIcon(R.mipmap.ic_notification);
int smallIconResId = getSmallIconResourceId(context, smallIcon);
notification.setSmallIcon(smallIconResId);
if (serverUrl != null && channelName.equals(senderName)) {
try {
String senderId = bundle.getString("sender_id");
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
if (avatar != null) {
notification.setLargeIcon(avatar);
}
@@ -399,31 +423,29 @@ public class CustomPushNotificationHelper {
}
}
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
try {
Response response;
final OkHttpClient client = new OkHttpClient();
Request request;
String url;
if (!TextUtils.isEmpty(urlOverride)) {
Request request = new Request.Builder().url(urlOverride).build();
request = new Request.Builder().url(urlOverride).build();
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
response = client.newCall(request).execute();
} else {
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
if (cached != null) {
Bitmap bitmap = cached.copy(cached.getConfig(), false);
return getCircleBitmap(bitmap);
}
String url = String.format("api/v4/users/%s/image", userId);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
Log.i("ReactNative", String.format("Fetch profile image %s", url));
response = Network.getSync(serverUrl, url, null);
request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.url(url)
.build();
}
Response response = client.newCall(request).execute();
if (response.code() == 200) {
assert response.body() != null;
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
}
return getCircleBitmap(bitmap);
}

View File

@@ -20,18 +20,13 @@ class DatabaseHelper {
val onlyServerUrl: String?
get() {
try {
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
return null
}
@@ -43,19 +38,14 @@ class DatabaseHelper {
}
fun getServerUrlForIdentifier(identifier: String): String? {
try {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
return null
}
@@ -73,25 +63,19 @@ class DatabaseHelper {
return resultMap
}
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
try {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
return null
}
@@ -164,23 +148,18 @@ class DatabaseHelper {
}
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
try {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
}
return null
}

View File

@@ -12,7 +12,6 @@ import com.mattermost.networkclient.APIClientModule;
import com.mattermost.networkclient.enums.RetryTypes;
import okhttp3.HttpUrl;
import okhttp3.Response;
public class Network {
@@ -36,16 +35,6 @@ public class Network {
clientModule.post(baseUrl, endpoint, options, promise);
}
public static Response getSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.getSync(baseUrl, endpoint, options);
}
public static Response postSync(String baseUrl, String endpoint, ReadableMap options) {
createClientIfNeeded(baseUrl);
return clientModule.postSync(baseUrl, endpoint, options);
}
private static void createClientOptions() {
WritableMap headers = Arguments.createMap();
headers.putString("X-Requested-With", "XMLHttpRequest");

View File

@@ -145,9 +145,9 @@ public class NotificationHelper {
for (final StatusBarNotification status : statusNotifications) {
Bundle bundle = status.getNotification().extras;
if (isThreadNotification) {
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
hasMore = bundle.getString("root_id").equals(rootId);
} else {
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
hasMore = bundle.getString("channel_id").equals(channelId);
}
if (hasMore) break;
}

View File

@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.util.Objects;
@@ -16,6 +17,7 @@ import com.mattermost.helpers.DatabaseHelper;
import com.mattermost.helpers.Network;
import com.mattermost.helpers.NotificationHelper;
import com.mattermost.helpers.PushNotificationDataHelper;
import com.mattermost.helpers.ResolvePromise;
import com.mattermost.share.ShareModule;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotification;
@@ -29,6 +31,7 @@ public class CustomPushNotification extends PushNotification {
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
dataHelper = new PushNotificationDataHelper(context);
try {
@@ -54,31 +57,27 @@ public class CustomPushNotification extends PushNotification {
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
if (ackId != null && serverUrl != null) {
Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded);
if (isIdLoaded && response != null) {
Bundle current = mNotificationProps.asBundle();
if (!current.containsKey("server_url")) {
response.putString("server_url", serverUrl);
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (isIdLoaded) {
Bundle response = (Bundle) value;
if (value != null) {
response.putString("server_url", serverUrl);
Bundle current = mNotificationProps.asBundle();
current.putAll(response);
mNotificationProps = createProps(current);
}
}
}
current.putAll(response);
mNotificationProps = createProps(current);
}
@Override
public void reject(String code, String message) {
Log.e("ReactNative", code + ": " + message);
}
});
}
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void finishProcessingNotification(String serverUrl, String type, String channelId, int notificationId, Boolean isReactInit) {
switch (type) {
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
@@ -119,6 +118,16 @@ public class CustomPushNotification extends PushNotification {
}
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
final Notification notification = buildNotification(pendingIntent);
@@ -140,6 +149,10 @@ public class CustomPushNotification extends PushNotification {
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
}
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(mContext, ackId, serverUrl, postId, type, isIdLoaded, promise);
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}

View File

@@ -1,58 +0,0 @@
package com.mattermost.rnbeta
import android.app.Activity
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
class FoldableObserver(private val activity: Activity) {
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
public fun onCreate() {
observable = WindowInfoTracker.getOrCreate(activity)
.windowLayoutInfoObservable(activity)
}
public fun onStart() {
if (disposable?.isDisposed == true) {
onCreate()
}
disposable = observable.observeOn(AndroidSchedulers.mainThread())
.subscribe { layoutInfo ->
val splitViewModule = SplitViewModule.getInstance()
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
foldingFeature?.state === FoldingFeature.State.FLAT ->
splitViewModule?.setDeviceFolded(false)
isTableTopPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
isBookPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
else -> {
splitViewModule?.setDeviceFolded(true)
}
}
}
}
public fun onStop() {
disposable?.dispose()
}
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}
private fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}
}

View File

@@ -12,9 +12,9 @@ import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
public class MainActivity extends NavigationActivity {
private boolean HWKeyboardConnected = false;
private FoldableObserver foldableObserver = new FoldableObserver(this);
@Override
protected String getMainComponentName() {
@@ -43,19 +43,6 @@ public class MainActivity extends NavigationActivity {
super.onCreate(null);
setContentView(R.layout.launch_screen);
setHWKeyboardConnected();
foldableObserver.onCreate();
}
@Override
protected void onStart() {
super.onStart();
foldableObserver.onStart();
}
@Override
protected void onStop() {
super.onStop();
foldableObserver.onStop();
}
@Override

View File

@@ -1,9 +1,7 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
@@ -44,6 +42,7 @@ public class MainApplication extends NavigationApplication implements INotificat
public static MainApplication instance;
public Boolean sharedExtensionIsOpened = false;
private final ReactNativeHost mReactNativeHost =
new DefaultReactNativeHost(this) {
@Override
@@ -70,8 +69,6 @@ public class MainApplication extends NavigationApplication implements INotificat
return ShareModule.getInstance(reactContext);
case "Notifications":
return NotificationsModule.getInstance(instance, reactContext);
case "SplitView":
return SplitViewModule.Companion.getInstance(reactContext);
default:
throw new IllegalArgumentException("Could not find module " + name);
}
@@ -84,7 +81,6 @@ public class MainApplication extends NavigationApplication implements INotificat
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
map.put("SplitView", new ReactModuleInfo("SplitView", "com.mattermost.rnbeta.SplitViewModule", false, false, false, false, false));
return map;
};
}

View File

@@ -133,6 +133,19 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
promise.resolve(map);
}
@ReactMethod
public void isRunningInSplitView(final Promise promise) {
WritableMap result = Arguments.createMap();
Activity current = getCurrentActivity();
if (current != null) {
result.putBoolean("isSplitView", current.isInMultiWindowMode());
} else {
result.putBoolean("isSplitView", false);
}
promise.resolve(result);
}
@ReactMethod
public void saveFile(String path, final Promise promise) {
Uri contentUri;

View File

@@ -8,16 +8,28 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import android.util.Log;
import java.io.IOException;
import java.util.Objects;
import okhttp3.Call;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.json.JSONException;
import com.mattermost.helpers.*;
import com.facebook.react.bridge.ReactApplicationContext;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
@@ -41,7 +53,12 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
final String serverUrl = bundle.getString("server_url");
if (serverUrl != null) {
replyToMessage(serverUrl, notificationId, message);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
if (token != null) {
replyToMessage(serverUrl, token, notificationId, message);
}
} else {
onReplyFailed(notificationId);
}
@@ -50,7 +67,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
}
}
protected void replyToMessage(final String serverUrl, final int notificationId, final CharSequence message) {
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
final String channelId = bundle.getString("channel_id");
final String postId = bundle.getString("post_id");
String rootId = bundle.getString("root_id");
@@ -58,53 +75,63 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
rootId = postId;
}
if (serverUrl == null) {
if (token == null || serverUrl == null) {
onReplyFailed(notificationId);
return;
}
WritableMap headers = Arguments.createMap();
headers.putString("Content-Type", "application/json");
WritableMap body = Arguments.createMap();
body.putString("channel_id", channelId);
body.putString("message", message.toString());
body.putString("root_id", rootId);
WritableMap options = Arguments.createMap();
options.putMap("headers", headers);
options.putMap("body", body);
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
String json = buildReplyPost(channelId, rootId, message.toString());
RequestBody body = RequestBody.create(json, JSON);
String postsEndpoint = "/api/v4/posts?set_online=false";
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
String url = String.format("%s%s", serverUrl.replaceAll("/$", ""), postsEndpoint);
Log.i("ReactNative", String.format("Reply URL=%s", url));
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void resolve(@Nullable Object value) {
if (value != null) {
public void onFailure(@NonNull Call call, @NonNull IOException e) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void onResponse(@NonNull Call call, @NonNull final Response response) throws IOException {
if (response.isSuccessful()) {
onReplySuccess(notificationId, message);
Log.i("ReactNative", "Reply SUCCESS");
} else {
Log.i("ReactNative", "Reply FAILED resolved without value");
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s",
response.code(),
Objects.requireNonNull(response.body()).string()
)
);
onReplyFailed(notificationId);
}
}
@Override
public void reject(Throwable reason) {
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.getMessage()));
onReplyFailed(notificationId);
}
@Override
public void reject(String code, String message) {
Log.i("ReactNative",
String.format("Reply FAILED status %s BODY %s", code, message)
);
onReplyFailed(notificationId);
}
});
}
protected String buildReplyPost(String channelId, String rootId, String message) {
try {
JSONObject json = new JSONObject();
json.put("channel_id", channelId);
json.put("message", message);
json.put("root_id", rootId);
return json.toString();
} catch(JSONException e) {
return "{}";
}
}
protected void onReplyFailed(int notificationId) {
recreateNotification(notificationId, "Message failed to send.");
}

View File

@@ -1,60 +1,135 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import java.lang.System;
import java.util.Objects;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.mattermost.helpers.*;
import okhttp3.Response;
public class ReceiptDelivery {
private static final String[] ackKeys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
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 String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
WritableMap options = Arguments.createMap();
WritableMap headers = Arguments.createMap();
WritableMap body = Arguments.createMap();
headers.putString("Content-Type", "application/json");
options.putMap("headers", headers);
body.putString("id", ackId);
body.putDouble("received_at", System.currentTimeMillis());
body.putString("platform", "android");
body.putString("type", type);
body.putString("post_id", postId);
body.putBoolean("is_id_loaded", isIdLoaded);
options.putMap("body", body);
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
}
try (Response response = Network.postSync(serverUrl, "api/v4/notifications/ack", options)) {
String responseBody = Objects.requireNonNull(response.body()).string();
JSONObject jsonResponse = new JSONObject(responseBody);
return parseAckResponse(jsonResponse);
} catch (Exception e) {
e.printStackTrace();
return null;
protected static void execute(String serverUrl, String postId, String token, String ackId, String type, boolean isIdLoaded, ResolvePromise promise) {
if (token == null) {
promise.reject("Receipt delivery failure", "Invalid token");
return;
}
if (serverUrl == null) {
promise.reject("Receipt delivery failure", "Invalid server URL");
}
JSONObject json;
long receivedAt = System.currentTimeMillis();
try {
json = new JSONObject();
json.put("id", ackId);
json.put("received_at", receivedAt);
json.put("platform", "android");
json.put("type", type);
json.put("post_id", postId);
json.put("is_id_loaded", isIdLoaded);
} catch (JSONException e) {
Log.e("ReactNative", "Receipt delivery failed to build json payload");
promise.reject("Receipt delivery failure", e.toString());
return;
}
final HttpUrl url;
if (serverUrl != null) {
url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(json.toString(), JSON);
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
makeServerRequest(client, request, isIdLoaded, 0, promise);
}
}
}
public static Bundle parseAckResponse(JSONObject jsonResponse) {
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
try {
Response response = client.newCall(request).execute();
String responseBody = Objects.requireNonNull(response.body()).string();
if (response.code() != 200) {
switch (response.code()) {
case 302:
promise.reject("Receipt delivery failure", "StatusFound");
return;
case 400:
promise.reject("Receipt delivery failure", "StatusBadRequest");
return;
case 401:
promise.reject("Receipt delivery failure", "Unauthorized");
case 403:
promise.reject("Receipt delivery failure", "Forbidden");
return;
case 500:
promise.reject("Receipt delivery failure", "StatusInternalServerError");
return;
case 501:
promise.reject("Receipt delivery failure", "StatusNotImplemented");
return;
}
throw new Exception(responseBody);
}
JSONObject jsonResponse = new JSONObject(responseBody);
Bundle bundle = new Bundle();
for (String key : ackKeys) {
String[] keys = new String[]{"post_id", "root_id", "category", "message", "team_id", "channel_id", "channel_name", "type", "sender_id", "sender_name", "version"};
for (String key : keys) {
if (jsonResponse.has(key)) {
bundle.putString(key, jsonResponse.getString(key));
}
}
return bundle;
promise.resolve(bundle);
} catch (Exception e) {
e.printStackTrace();
return null;
Log.e("ReactNative", "Receipt delivery failed to send");
if (isIdLoaded) {
try {
reRequestCount++;
if (reRequestCount < FIBONACCI_BACKOFF.length) {
Log.i("ReactNative", "Retry attempt " + reRequestCount + " with backoff delay: " + FIBONACCI_BACKOFF[reRequestCount] + " seconds");
Thread.sleep(FIBONACCI_BACKOFF[reRequestCount] * 1000);
makeServerRequest(client, request, true, reRequestCount, promise);
}
} catch(InterruptedException ie) {
ie.printStackTrace();
}
}
promise.reject("Receipt delivery failure", e.toString());
}
}
}

View File

@@ -1,73 +0,0 @@
package com.mattermost.rnbeta
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var isDeviceFolded: Boolean = false
private var listenerCount = 0
companion object {
private var instance: SplitViewModule? = null
fun getInstance(reactContext: ReactApplicationContext): SplitViewModule {
if (instance == null) {
instance = SplitViewModule(reactContext)
} else {
instance!!.reactContext = reactContext
}
return instance!!
}
fun getInstance(): SplitViewModule? {
return instance
}
}
override fun getName() = "SplitView"
fun sendEvent(eventName: String,
params: WritableMap?) {
reactContext
.getJSModule(RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
if (currentActivity != null) {
val deviceResolver = DeviceTypeResolver(this.reactContext)
val map = Arguments.createMap()
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
map.putBoolean("isTablet", deviceResolver.isTablet)
return map
}
return null
}
fun setDeviceFolded(folded: Boolean) {
val map = getSplitViewResults(folded)
if (listenerCount > 0 && isDeviceFolded != folded) {
sendEvent("SplitViewChanged", map)
}
isDeviceFolded = folded
}
@ReactMethod
fun isRunningInSplitView(promise: Promise) {
promise.resolve(getSplitViewResults(isDeviceFolded))
}
@ReactMethod
fun addListener(eventName: String) {
listenerCount += 1
}
@ReactMethod
fun removeListeners(count: Int) {
listenerCount -= count
}
}

View File

@@ -1,19 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
* directory of this source tree.
*/
package com.mattermost.flipper;
import android.content.Context;
import com.facebook.react.ReactInstanceManager;
/**
* Class responsible of loading Flipper inside your React Native application. This is the release
* flavor of it so it's empty as we don't want to load Flipper.
*/
public class ReactNativeFlipper {
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
// Do nothing as we don't want to initialize Flipper on Release.
}
}

View File

@@ -9,6 +9,7 @@ import {queryMyTeams} from '@queries/servers/team';
import {isDMorGM} from '@utils/channel';
import {logError} from '@utils/log';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
export const deleteCategory = async (serverUrl: string, categoryId: string) => {
@@ -39,7 +40,7 @@ export async function storeCategories(serverUrl: string, categories: CategoryWit
}
if (models.length > 0) {
await operator.batchRecords(models, 'storeCategories');
await operator.batchRecords(models);
}
return {models};
@@ -79,6 +80,7 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
return {error: 'no current user id'};
}
const models: Model[] = [];
const categoriesWithChannels: CategoryWithChannels[] = [];
if (isDMorGM(channel)) {
@@ -98,12 +100,13 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
cwc.channel_ids.unshift(channel.id);
categoriesWithChannels.push(cwc);
}
const ccModels = await prepareCategoryChannels(operator, categoriesWithChannels);
models.push(...ccModels);
}
const models = await prepareCategoryChannels(operator, categoriesWithChannels);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'addChannelToDefaultCategory');
await operator.batchRecords(models);
}
return {models};

View File

@@ -13,7 +13,7 @@ import {
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareCommonSystemValues, PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
import {addChannelToTeamHistory, addTeamToTeamHistory, getTeamById, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getCurrentUser, queryUsersById} from '@queries/servers/user';
@@ -82,7 +82,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'switchToChannel');
await operator.batchRecords(models);
}
if (isTabletDevice) {
@@ -124,7 +124,7 @@ export async function removeCurrentUserFromChannel(serverUrl: string, channelId:
await removeChannelFromTeamHistory(operator, teamId, channel.id, false);
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'removeCurrentUserFromChannel');
await operator.batchRecords(models);
}
}
return {models};
@@ -145,7 +145,7 @@ export async function setChannelDeleteAt(serverUrl: string, channelId: string, d
const model = channel.prepareUpdate((c) => {
c.deleteAt = deleteAt;
});
await operator.batchRecords([model], 'setChannelDeleteAt');
await operator.batchRecords([model]);
} catch (error) {
logError('FAILED TO BATCH CHANGES FOR CHANNEL DELETE AT', error);
}
@@ -179,7 +179,7 @@ export async function markChannelAsViewed(serverUrl: string, channelId: string,
});
PushNotifications.removeChannelNotifications(serverUrl, channelId);
if (!prepareRecordsOnly) {
await operator.batchRecords([member], 'markChannelAsViewed');
await operator.batchRecords([member]);
}
return {member};
@@ -206,7 +206,7 @@ export async function markChannelAsUnread(serverUrl: string, channelId: string,
m.isUnread = true;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([member], 'markChannelAsUnread');
await operator.batchRecords([member]);
}
return {member};
@@ -226,7 +226,7 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
member.prepareUpdate((m) => {
m.messageCount = 0;
});
await operator.batchRecords([member], 'resetMessageCount');
await operator.batchRecords([member]);
return member;
} catch (error) {
@@ -254,7 +254,7 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
}
if (flattenedModels.length) {
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
await operator.batchRecords(flattenedModels);
}
return {models: flattenedModels};
@@ -273,7 +273,7 @@ export async function updateMyChannelFromWebsocket(serverUrl: string, channelMem
m.roles = channelMember.roles;
});
if (!prepareRecordsOnly) {
operator.batchRecords([member], 'updateMyChannelFromWebsocket');
operator.batchRecords([member]);
}
}
return {model: member};
@@ -293,7 +293,7 @@ export async function updateChannelInfoFromChannel(serverUrl: string, channel: C
}],
prepareRecordsOnly: true});
if (!prepareRecordsOnly) {
operator.batchRecords(newInfo, 'updateChannelInfoFromChannel');
operator.batchRecords(newInfo);
}
return {model: newInfo};
} catch (error) {
@@ -317,7 +317,7 @@ export async function updateLastPostAt(serverUrl: string, channelId: string, las
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel], 'updateLastPostAt');
await operator.batchRecords([myChannel]);
}
return {member: myChannel};
@@ -345,7 +345,7 @@ export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId:
});
if (!prepareRecordsOnly) {
await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt');
await operator.batchRecords([myChannel]);
}
return {member: myChannel};
@@ -369,7 +369,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
const license = await getLicense(database);
const config = await getConfig(database);
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const displaySettings = getTeammateNameDisplaySetting(preferences, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const models: Model[] = [];
for await (const channel of channels) {
@@ -403,7 +403,7 @@ export async function updateChannelsDisplayName(serverUrl: string, channels: Cha
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'updateChannelsDisplayName');
await operator.batchRecords(models);
}
return {models};

View File

@@ -18,15 +18,18 @@ export async function updateDraftFile(serverUrl: string, channelId: string, root
return {error: 'file not found'};
}
file.is_voice_recording = draft.files[i].is_voice_recording;
// We create a new list to make sure we re-render the draft input.
const newFiles = [...draft.files];
newFiles[i] = file;
draft.prepareUpdate((d) => {
d.files = newFiles;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([draft], 'updateDraftFile');
await operator.batchRecords([draft]);
}
return {draft};
@@ -58,7 +61,7 @@ export async function removeDraftFile(serverUrl: string, channelId: string, root
}
if (!prepareRecordsOnly) {
await operator.batchRecords([draft], 'removeDraftFile');
await operator.batchRecords([draft]);
}
return {draft};
@@ -99,7 +102,7 @@ export async function updateDraftMessage(serverUrl: string, channelId: string, r
}
if (!prepareRecordsOnly) {
await operator.batchRecords([draft], 'updateDraftMessage');
await operator.batchRecords([draft]);
}
return {draft};
@@ -129,7 +132,7 @@ export async function addFilesToDraft(serverUrl: string, channelId: string, root
});
if (!prepareRecordsOnly) {
await operator.batchRecords([draft], 'addFilesToDraft');
await operator.batchRecords([draft]);
}
return {draft};

View File

@@ -127,14 +127,14 @@ export async function removePost(serverUrl: string, post: PostModel | Post) {
}
if (removeModels.length) {
await operator.batchRecords(removeModels, 'removePost (combined user activity)');
await operator.batchRecords(removeModels);
}
} else {
const postModel = await getPostById(database, post.id);
if (postModel) {
const preparedPost = await prepareDeletePost(postModel);
if (preparedPost.length) {
await operator.batchRecords(preparedPost, 'removePost');
await operator.batchRecords(preparedPost);
}
}
}
@@ -162,7 +162,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
});
if (!prepareRecordsOnly) {
await operator.batchRecords([dbPost], 'markPostAsDeleted');
await operator.batchRecords([dbPost]);
}
return {model};
} catch (error) {
@@ -229,7 +229,7 @@ export async function storePostsForChannel(
}
if (models.length && !prepareRecordsOnly) {
await operator.batchRecords(models, 'storePostsForChannel');
await operator.batchRecords(models);
}
return {models};

View File

@@ -22,7 +22,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
models.push(...system);
}
if (models.length) {
await operator.batchRecords(models, 'removeUserFromTeam');
await operator.batchRecords(models);
}
}
@@ -61,7 +61,7 @@ export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: st
}
}
await operator.batchRecords(models, 'addSearchToTeamHistory');
await operator.batchRecords(models);
return {searchModel};
} catch (error) {
logError('Failed addSearchToTeamSearchHistory', error);

View File

@@ -40,7 +40,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
models.push(...history);
if (!prepareRecordsOnly) {
await operator.batchRecords(models, 'switchToGlobalThreads');
await operator.batchRecords(models);
}
const isTabletDevice = await isTablet();
@@ -84,7 +84,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
currentChannelId: channel.id,
});
if (models.length) {
await operator.batchRecords(models, 'switchToThread');
await operator.batchRecords(models);
}
} else {
const modelPromises: Array<Promise<Model[]>> = [];
@@ -97,7 +97,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
modelPromises.push(prepareCommonSystemValues(operator, commonValues));
const models = (await Promise.all(modelPromises)).flat();
if (models.length) {
await operator.batchRecords(models, 'switchToThread');
await operator.batchRecords(models);
}
}
@@ -201,7 +201,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
}
if (!prepareRecordsOnly) {
await operator.batchRecords(models, 'createThreadFromNewPost');
await operator.batchRecords(models);
}
return {models};
@@ -257,7 +257,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
}
if (!prepareRecordsOnly) {
await operator.batchRecords(models, 'processReceivedThreads');
await operator.batchRecords(models);
}
return {models};
} catch (error) {
@@ -277,7 +277,7 @@ export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, p
record.viewedAt = Date.now();
}));
if (!prepareRecordsOnly) {
await operator.batchRecords(models, 'markTeamThreadsAsRead');
await operator.batchRecords(models);
}
return {models};
} catch (error) {
@@ -300,7 +300,7 @@ export async function markThreadAsViewed(serverUrl: string, threadId: string, pr
});
if (!prepareRecordsOnly) {
await operator.batchRecords([thread], 'markThreadAsViewed');
await operator.batchRecords([thread]);
}
return {model: thread};
@@ -327,7 +327,7 @@ export async function updateThread(serverUrl: string, threadId: string, updatedT
record.unreadReplies = updatedThread.unread_replies ?? record.unreadReplies;
});
if (!prepareRecordsOnly) {
await operator.batchRecords([model], 'updateThread');
await operator.batchRecords([model]);
}
return {model};
} catch (error) {
@@ -341,7 +341,7 @@ export async function updateTeamThreadsSync(serverUrl: string, data: TeamThreads
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const models = await operator.handleTeamThreadsSync({data: [data], prepareRecordsOnly});
if (!prepareRecordsOnly) {
await operator.batchRecords(models, 'updateTeamThreadsSync');
await operator.batchRecords(models);
}
return {models};
} catch (error) {

View File

@@ -22,7 +22,7 @@ export async function setCurrentUserStatusOffline(serverUrl: string) {
}
user.prepareStatus(General.OFFLINE);
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
await operator.batchRecords([user]);
return null;
} catch (error) {
logError('Failed setCurrentUserStatusOffline', error);
@@ -54,7 +54,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel
}
}
await operator.batchRecords(models, 'updateLocalCustomStatus');
await operator.batchRecords(models);
return {};
} catch (error) {
@@ -97,37 +97,27 @@ export const updateRecentCustomStatuses = async (serverUrl: string, customStatus
export const updateLocalUser = async (
serverUrl: string,
userDetails: Partial<UserProfile> & { status?: string},
userId?: string,
) => {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let user: UserModel | undefined;
if (userId) {
user = await getUserById(database, userId);
} else {
user = await getCurrentUser(database);
}
const user = await getCurrentUser(database);
if (user) {
const u = user;
await database.write(async () => {
await u.update((userRecord: UserModel) => {
userRecord.authService = userDetails.auth_service ?? u.authService;
userRecord.email = userDetails.email ?? u.email;
userRecord.firstName = userDetails.first_name ?? u.firstName;
userRecord.lastName = userDetails.last_name ?? u.lastName;
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? u.lastPictureUpdate;
userRecord.locale = userDetails.locale ?? u.locale;
userRecord.nickname = userDetails.nickname ?? u.nickname;
userRecord.notifyProps = userDetails.notify_props ?? u.notifyProps;
userRecord.position = userDetails?.position ?? u.position;
userRecord.props = userDetails.props ?? u.props;
userRecord.roles = userDetails.roles ?? u.roles;
userRecord.status = userDetails?.status ?? u.status;
userRecord.timezone = userDetails.timezone ?? u.timezone;
userRecord.username = userDetails.username ?? u.username;
await user.update((userRecord: UserModel) => {
userRecord.authService = userDetails.auth_service ?? user.authService;
userRecord.email = userDetails.email ?? user.email;
userRecord.firstName = userDetails.first_name ?? user.firstName;
userRecord.lastName = userDetails.last_name ?? user.lastName;
userRecord.lastPictureUpdate = userDetails.last_picture_update ?? user.lastPictureUpdate;
userRecord.locale = userDetails.locale ?? user.locale;
userRecord.nickname = userDetails.nickname ?? user.nickname;
userRecord.notifyProps = userDetails.notify_props ?? user.notifyProps;
userRecord.position = userDetails?.position ?? user.position;
userRecord.props = userDetails.props ?? user.props;
userRecord.roles = userDetails.roles ?? user.roles;
userRecord.status = userDetails?.status ?? user.status;
userRecord.timezone = userDetails.timezone ?? user.timezone;
userRecord.username = userDetails.username ?? user.username;
});
});
}

View File

@@ -7,7 +7,6 @@ import {DeviceEventEmitter} from 'react-native';
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
import {removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
import {switchToGlobalThreads} from '@actions/local/thread';
import {updateLocalUser} from '@actions/local/user';
import {loadCallForChannel} from '@calls/actions/calls';
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
import DatabaseManager from '@database/manager';
@@ -16,8 +15,8 @@ import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import AppsManager from '@managers/apps_manager';
import NetworkManager from '@managers/network_manager';
import {getActiveServer} from '@queries/app/servers';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId} from '@queries/servers/channel';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getCommonSystemValues, getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getLicense, setCurrentChannelId, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getNthLastChannelFromTeam, getMyTeamById, getTeamByName, queryMyTeams, removeChannelFromTeamHistory} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
@@ -36,10 +35,9 @@ import {setDirectChannelVisible} from './preference';
import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session';
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
import {fetchProfilesInChannel, fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl';
@@ -51,99 +49,6 @@ export type MyChannelsRequest = {
error?: unknown;
}
export type ChannelMembersRequest = {
members?: ChannelMembership[];
error?: unknown;
}
export async function removeMemberFromChannel(serverUrl: string, channelId: string, userId: string) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
await client.removeFromChannel(userId, channelId);
await deleteChannelMembership(operator, userId, channelId);
return {error: undefined};
} catch (error) {
logError('removeMemberFromChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchChannelMembersByIds(serverUrl: string, channelId: string, userIds: string[], fetchOnly = false): Promise<ChannelMembersRequest> {
try {
const client = NetworkManager.getClient(serverUrl);
const members = await client.getChannelMembersByIds(channelId, userIds);
if (!fetchOnly) {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
if (operator && members.length) {
const memberships = members.map((u) => ({
channel_id: channelId,
user_id: u.user_id,
scheme_admin: u.scheme_admin,
}));
await operator.handleChannelMembership({
channelMemberships: memberships,
prepareRecordsOnly: false,
});
}
}
return {members};
} catch (error) {
logError('fetchChannelMembersByIds', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
}
export async function updateChannelMemberSchemeRoles(serverUrl: string, channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.updateChannelMemberSchemeRoles(channelId, userId, isSchemeUser, isSchemeAdmin);
if (!fetchOnly) {
return getMemberInChannel(serverUrl, channelId, userId);
}
return {error: undefined};
} catch (error) {
logError('updateChannelMemberSchemeRoles', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function getMemberInChannel(serverUrl: string, channelId: string, userId: string, fetchOnly = false) {
try {
const client = NetworkManager.getClient(serverUrl);
const member = await client.getMemberInChannel(channelId, userId);
if (!fetchOnly) {
updateLocalUser(serverUrl, member, userId);
}
return {member, error: undefined};
} catch (error) {
logError('getMemberInChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
}
export async function fetchChannelMemberships(serverUrl: string, channelId: string, options: GetUsersOptions, fetchOnly = false) {
const {users = []} = await fetchProfilesInChannel(serverUrl, channelId, undefined, options, fetchOnly);
const userIds = users.map((u) => u.id);
// MM-49896 https://mattermost.atlassian.net/browse/MM-49896
// We are not sure the getChannelMembers API returns the same members
// from getProfilesInChannel. This guarantees a 1:1 match of the
// user IDs
const {members = []} = await fetchChannelMembersByIds(serverUrl, channelId, userIds, true);
return {users, members};
}
export async function addMembersToChannel(serverUrl: string, channelId: string, userIds: string[], postRootId = '', fetchOnly = false) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
@@ -174,7 +79,7 @@ export async function addMembersToChannel(serverUrl: string, channelId: string,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'addMembersToChannel');
await operator.batchRecords(models.flat());
}
return {channelMemberships};
} catch (error) {
@@ -251,7 +156,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
models.push(...categoriesModels.models);
}
if (models.length) {
await operator.batchRecords(models, 'createChannel');
await operator.batchRecords(models);
}
fetchChannelStats(serverUrl, channelData.id, false);
EphemeralStore.creatingChannel = false;
@@ -296,7 +201,7 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
models.push(channel);
}
if (models?.length) {
await operator.batchRecords(models.flat(), 'patchChannel');
await operator.batchRecords(models.flat());
}
return {channel: channelData};
} catch (error) {
@@ -343,7 +248,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
models.push(...removeUserModels);
}
await operator.batchRecords(models, 'leaveChannel');
await operator.batchRecords(models);
if (isTabletDevice) {
switchToLastChannel(serverUrl);
@@ -393,7 +298,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
}));
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
await operator.batchRecords(models.flat());
}
return {user};
@@ -484,7 +389,7 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
const models = (chModels || []).concat(catModels || []);
if (models.length) {
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
await operator.batchRecords(models);
}
setTeamLoading(serverUrl, false);
}
@@ -533,37 +438,38 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
}
export async function fetchMissingDirectChannelsInfo(serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, currentUserId?: string, fetchOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
const channelIds = new Set(directChannels.map((c) => c.id));
const storedChannels = await queryChannelsById(database, Array.from(channelIds)).fetch();
const storedChannelsMap = new Map(storedChannels.map((c) => [c.id, c]));
const {database} = operator;
const displayNameByChannel: Record<string, string> = {};
const users: UserProfile[] = [];
const updatedChannels = new Set<Channel>();
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name && !storedChannelsMap.get(c.id)?.displayName) {
dmWithoutDisplayName.add(c.id);
}
continue;
const dms: Channel[] = [];
const dmIds: string[] = [];
const dmWithoutDisplayName = new Set<string>();
const gms: Channel[] = [];
for (const c of directChannels) {
if (c.type === General.DM_CHANNEL) {
dms.push(c);
dmIds.push(c.id);
if (!c.display_name) {
dmWithoutDisplayName.add(c.id);
}
gms.push(c);
continue;
}
gms.push(c);
}
try {
const currentUser = await getCurrentUser(database);
// let's filter those channels that we already have the users
const membersCount = await getMembersCountByChannelsId(database, dmIds);
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 && dmWithoutDisplayName.has(id));
const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 || dmWithoutDisplayName.has(id));
const results = await Promise.all([
profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false) : Promise.resolve({data: undefined}),
fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false),
@@ -609,7 +515,7 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
await operator.batchRecords(models.flat());
}
return {directChannels: updatedChannelsArray, users};
@@ -624,7 +530,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
return {error: `${serverUrl} database not found`};
}
const preferences = await queryDisplayNamePreferences(database).fetch();
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
const config = await getConfig(database);
const license = await getLicense(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
@@ -687,7 +593,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
}
if (flattenedModels?.length > 0) {
try {
await operator.batchRecords(flattenedModels, 'joinChannel');
await operator.batchRecords(flattenedModels);
} catch {
logError('FAILED TO BATCH CHANNELS');
}
@@ -870,7 +776,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
if (displayName) {
created.display_name = displayName;
} else {
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
@@ -913,7 +819,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
models.push(...userModels);
}
await operator.batchRecords(models, 'createDirectChannel');
await operator.batchRecords(models);
EphemeralStore.creatingDMorGMTeammates = [];
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
return {data: created};
@@ -1012,7 +918,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
return {data: created};
}
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const license = await getLicense(database);
const config = await getConfig(database);
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
@@ -1047,7 +953,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
}
models.push(...userModels);
operator.batchRecords(models, 'createGroupChannel');
operator.batchRecords(models);
}
}
EphemeralStore.creatingDMorGMTeammates = [];
@@ -1392,7 +1298,7 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
channel.prepareUpdate((c) => {
c.type = General.PRIVATE_CHANNEL;
});
await operator.batchRecords([channel], 'convertChannelToPrivate');
await operator.batchRecords([channel]);
}
return {error: undefined};
} catch (error) {

View File

@@ -33,7 +33,7 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
// clear lastUnreadChannelId
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
if (removeLastUnreadChannelId) {
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
await operator.batchRecords(removeLastUnreadChannelId);
}
const {database} = operator;
@@ -58,14 +58,14 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false)
currentChannelId: isTabletDevice ? initialChannelId : undefined,
});
if (me?.length) {
await operator.batchRecords(me, 'appEntry - upgrade store me');
await operator.batchRecords(me);
}
}
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models, 'appEntry');
await operator.batchRecords(models);
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);

View File

@@ -178,7 +178,7 @@ export const fetchAppEntryData = async (serverUrl: string, sinceArg: number, ini
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
const config = await getConfig(database);
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
@@ -431,7 +431,7 @@ const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
const models = (await Promise.all(modelPromises)).flat();
if (models.length) {
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
await operator.batchRecords(models);
}
}

View File

@@ -82,7 +82,7 @@ export async function deferredAppEntryGraphQLActions(
modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true}));
}
const models = (await Promise.all(modelPromises)).flat();
operator.batchRecords(models, 'deferredAppEntryActions');
operator.batchRecords(models);
setTimeout(() => {
if (result.chData?.channels?.length && result.chData.memberships?.length) {
@@ -217,7 +217,7 @@ export const entryGQL = async (serverUrl: string, currentTeamId?: string, curren
if (!teamData.teams.length) {
initialTeamId = '';
} else if (!initialTeamId || !teamData.teams.find((t) => t.id === currentTeamId && t.delete_at === 0)) {
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
}
const gqlRoles = [

View File

@@ -68,7 +68,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
setCurrentTeamAndChannelId(operator, initialTeamId, '');
}
await operator.batchRecords(models, 'loginEntry');
await operator.batchRecords(models);
setTeamLoading(serverUrl, false);
const config = clData.config || {} as ClientConfig;

View File

@@ -3,11 +3,11 @@
import {switchToChannelById} from '@actions/remote/channel';
import {fetchAndSwitchToThread} from '@actions/remote/thread';
import {Screens} from '@constants';
import {Preferences, Screens} from '@constants';
import {getDefaultThemeByAppearance} from '@context/theme';
import DatabaseManager from '@database/manager';
import {getMyChannel} from '@queries/servers/channel';
import {queryThemePreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
@@ -53,7 +53,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
// When opening the app from a push notification the theme may not be set in the EphemeralStore
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
// not match the theme
const themes = await queryThemePreferences(database, teamId).fetch();
const themes = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, teamId).fetch();
let theme = getDefaultThemeByAppearance();
if (themes.length) {
theme = setThemeDefaults(JSON.parse(themes[0].value) as Theme);
@@ -136,7 +136,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
}
await operator.batchRecords(models, 'pushNotificationEntry');
await operator.batchRecords(models);
setTeamLoading(serverUrl, false);
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;

View File

@@ -3,7 +3,6 @@
import {DOWNLOAD_TIMEOUT} from '@constants/network';
import NetworkManager from '@managers/network_manager';
import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
@@ -33,11 +32,10 @@ export const uploadFile = (
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
} catch (error) {
logDebug('uploadFile', error);
return {error: error as ClientError};
}
return {cancel: client.uploadPostAttachment(file, channelId, onProgress, onComplete, onError, skipBytes)};
};
export const fetchPublicLink = async (serverUrl: string, fileId: string) => {

View File

@@ -83,7 +83,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupChannels], 'fetchGroupsForChannel');
await operator.batchRecords([...groups, ...groupChannels]);
}
return {groups, groupChannels};
@@ -110,7 +110,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupTeams], 'fetchGroupsForTeam');
await operator.batchRecords([...groups, ...groupTeams]);
}
return {groups, groupTeams};
@@ -136,7 +136,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
]);
if (!fetchOnly) {
await operator.batchRecords([...groups, ...groupMemberships], 'fetchGroupsForMember');
await operator.batchRecords([...groups, ...groupMemberships]);
}
return {groups, groupMemberships};

View File

@@ -128,7 +128,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
initialPostModels.push(...reactionModels);
}
await operator.batchRecords(initialPostModels, 'createPost - initial');
await operator.batchRecords(initialPostModels);
const isCRTEnabled = await getIsCRTEnabled(database);
@@ -167,7 +167,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'createPost - failure');
await operator.batchRecords(models);
}
return {data: true};
@@ -192,7 +192,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'createPost - success');
await operator.batchRecords(models);
newPost = created;
@@ -237,7 +237,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
p.props = newPost.props;
p.updateAt = timestamp;
});
await operator.batchRecords([post], 'retryFailedPost - first update');
await operator.batchRecords([post]);
const created = await client.createPost(newPost);
const models = await operator.handlePosts({
@@ -253,7 +253,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
models.push(member);
}
}
await operator.batchRecords(models, 'retryFailedPost - success update');
await operator.batchRecords(models);
} catch (error) {
if (isServerError(error) && (
error.server_error_id === ServerErrors.DELETED_ROOT_POST_ERROR ||
@@ -268,7 +268,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
failed: true,
};
});
await operator.batchRecords([post], 'retryFailedPost - error update');
await operator.batchRecords([post]);
}
return {error};
@@ -375,7 +375,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0,
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'fetchPosts');
await operator.batchRecords(models);
}
return result;
} catch (error) {
@@ -434,7 +434,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
}
}
await operator.batchRecords(models, 'fetchPostsBefore');
await operator.batchRecords(models);
} catch (error) {
logError('FETCH POSTS BEFORE ERROR', error);
}
@@ -489,7 +489,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'fetchPostsSince');
await operator.batchRecords(models);
}
return result;
} catch (error) {
@@ -621,7 +621,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'fetchPostThread');
await operator.batchRecords(models);
}
setFetchingThreadState(postId, false);
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
@@ -697,7 +697,7 @@ export async function fetchPostsAround(serverUrl: string, channelId: string, pos
models.push(...threadModels);
}
}
await operator.batchRecords(models, 'fetchPostsAround');
await operator.batchRecords(models);
}
return {posts: extractRecordsForTable<PostModel>(posts, MM_TABLES.SERVER.POST)};
@@ -751,7 +751,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
return mdls;
});
if (models.length) {
await operator.batchRecords(models, 'fetchMissingChannelsFromPosts');
await operator.batchRecords(models);
}
}
}
@@ -809,7 +809,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
}
}
await operator.batchRecords(models, 'fetchPostById');
await operator.batchRecords(models);
}
return {post};
@@ -1036,7 +1036,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
return mdls;
});
await operator.batchRecords(models, 'fetchSavedPosts');
await operator.batchRecords(models);
return {
order,
@@ -1118,7 +1118,7 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
return mdls;
});
await operator.batchRecords(models, 'fetchPinnedPosts');
await operator.batchRecords(models);
return {
order,

View File

@@ -5,12 +5,14 @@ import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getChannelById} from '@queries/servers/channel';
import {querySavedPostsPreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import {getUserIdFromChannelName} from '@utils/user';
import {forceLogoutIfNecessary} from './session';
const {CATEGORY_DIRECT_CHANNEL_SHOW, CATEGORY_GROUP_CHANNEL_SHOW, CATEGORY_FAVORITE_CHANNEL, CATEGORY_SAVED_POST} = Preferences;
export type MyPreferencesRequest = {
preferences?: PreferenceType[];
error?: unknown;
@@ -54,7 +56,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
try {
const userId = await getCurrentUserId(operator.database);
const favPref: PreferenceType = {
category: Preferences.CATEGORIES.FAVORITE_CHANNEL,
category: CATEGORY_FAVORITE_CHANNEL,
name: channelId,
user_id: userId,
value: String(isFavorite),
@@ -75,7 +77,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
const userId = await getCurrentUserId(operator.database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORIES.SAVED_POST,
category: CATEGORY_SAVED_POST,
name: postId,
value: 'true',
};
@@ -114,15 +116,23 @@ export const savePreference = async (serverUrl: string, preferences: PreferenceT
};
export const deleteSavedPost = async (serverUrl: string, postId: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const userId = await getCurrentUserId(database);
const records = await querySavedPostsPreferences(database, postId).fetch();
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const userId = await getCurrentUserId(operator.database);
const records = await queryPreferencesByCategoryAndName(operator.database, CATEGORY_SAVED_POST, postId).fetch();
const postPreferenceRecord = records.find((r) => postId === r.name);
const pref = {
user_id: userId,
category: Preferences.CATEGORIES.SAVED_POST,
category: CATEGORY_SAVED_POST,
name: postId,
value: 'true',
};
@@ -151,8 +161,7 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
const channel = await getChannelById(database, channelId);
if (channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL) {
const userId = await getCurrentUserId(database);
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
const category = channel.type === General.DM_CHANNEL ? CATEGORY_DIRECT_CHANNEL_SHOW : CATEGORY_GROUP_CHANNEL_SHOW;
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channelId;
const pref: PreferenceType = {
user_id: userId,
@@ -176,7 +185,7 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
const userId = await getCurrentUserId(database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORIES.EMOJI,
category: Preferences.CATEGORY_EMOJI,
name: Preferences.EMOJI_SKINTONE,
value: skinCode,
};

View File

@@ -51,7 +51,7 @@ export async function addReaction(serverUrl: string, postId: string, emojiName:
models.push(...recent);
}
await operator.batchRecords(models, 'addReaction');
await operator.batchRecords(models);
return {reaction};
}

View File

@@ -9,7 +9,7 @@ import {selectDefaultTeam} from '@helpers/api/team';
import NetworkManager from '@managers/network_manager';
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
import {prepareMyPreferences, queryDisplayNamePreferences} from '@queries/servers/preference';
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {prepareCommonSystemValues, getConfig, getLicense} from '@queries/servers/system';
import {prepareMyTeams} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
@@ -60,7 +60,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
// select initial team
if (!clData.error && !prefData.error && !teamData.error) {
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences!, Preferences.CATEGORIES.TEAMS_ORDER, '', '');
const teamOrderPreference = getPreferenceValue(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string;
const teamRoles: string[] = [];
const teamMembers = new Set<string>();
@@ -117,7 +117,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
),
])).flat();
await operator.batchRecords(models, 'retryInitialTeamAndChannel');
await operator.batchRecords(models);
const directChannels = chData!.channels!.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
@@ -157,7 +157,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
return {error: true};
}
const prefs = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const prefs = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const preferences: PreferenceType[] = prefs.map((p) => ({
category: p.category,
name: p.name,
@@ -196,7 +196,7 @@ export async function retryInitialChannel(serverUrl: string, teamId: string) {
prepareCommonSystemValues(operator, {currentChannelId: initialChannel?.id}),
])).flat();
await operator.batchRecords(models, 'retryInitialChannel');
await operator.batchRecords(models);
const directChannels = chData!.channels!.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);

View File

@@ -103,7 +103,7 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos
return mdls;
});
await operator.batchRecords(models, 'searchPosts');
await operator.batchRecords(models);
return {
order,
posts: postsArray,

View File

@@ -86,7 +86,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
prepareCategoriesAndCategoriesChannels(operator, categories || [], true),
])).flat();
await operator.batchRecords(models, 'addUserToTeam');
await operator.batchRecords(models);
setTeamLoading(serverUrl, false);
loadEventSent = false;
@@ -199,7 +199,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels.length > 0) {
await operator.batchRecords(flattenedModels, 'fetchMyTeams');
await operator.batchRecords(flattenedModels);
}
}
}
@@ -233,7 +233,7 @@ export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly =
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels, 'fetchMyTeam');
await operator.batchRecords(flattenedModels);
}
}
}
@@ -362,7 +362,7 @@ export async function fetchTeamByName(serverUrl: string, teamName: string, fetch
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const models = await operator.handleTeam({teams: [team], prepareRecordsOnly: true});
await operator.batchRecords(models, 'fetchTeamByName');
await operator.batchRecords(models);
}
}
@@ -441,7 +441,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
}
if (models.length) {
await operator.batchRecords(models, 'handleTeamChange');
await operator.batchRecords(models);
}
DeviceEventEmitter.emit(Events.TEAM_SWITCH, false);

View File

@@ -43,7 +43,7 @@ export async function updateTermsOfServiceStatus(serverUrl: string, id: string,
u.termsOfServiceId = '';
}
});
operator.batchRecords([currentUser], 'updateTermsOfServiceStatus');
operator.batchRecords([currentUser]);
}
return {resp};
} catch (error) {

View File

@@ -360,7 +360,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
const allNewThreads = await fetchThreads(
serverUrl,
teamId,
{deleted: true, since: syncData.latest + 1},
{deleted: true, since: syncData.latest},
);
if (allNewThreads.error) {
return {error: allNewThreads.error};
@@ -395,7 +395,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
if (!prepareRecordsOnly && models?.length) {
try {
await operator.batchRecords(models, 'syncTeamThreads');
await operator.batchRecords(models);
} catch (err) {
if (__DEV__) {
throw err;
@@ -460,7 +460,7 @@ export const loadEarlierThreads = async (serverUrl: string, teamId: string, last
if (!prepareRecordsOnly && models?.length) {
try {
await operator.batchRecords(models, 'loadEarlierThreads');
await operator.batchRecords(models);
} catch (err) {
if (__DEV__) {
throw err;

View File

@@ -74,7 +74,7 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyU
}
};
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, fetchOnly = false): Promise<ProfilesInChannelRequest> {
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, fetchOnly = false): Promise<ProfilesInChannelRequest> {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
@@ -83,7 +83,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
}
try {
const users = await client.getProfilesInChannel(channelId, options);
const users = await client.getProfilesInChannel(channelId);
const uniqueUsers = Array.from(new Set(users));
const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId);
if (!fetchOnly) {
@@ -102,13 +102,12 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
modelPromises.push(prepare);
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchProfilesInChannel');
await operator.batchRecords(models.flat());
}
}
return {channelId, users: filteredUsers};
} catch (error) {
logError('fetchProfilesInChannel', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {channelId, error};
}
@@ -178,7 +177,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchProfilesInGroupChannels');
await operator.batchRecords(models.flat());
}
return {data};
@@ -199,7 +198,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
const data: ProfilesInChannelRequest[] = [];
for await (const cIds of channels) {
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true));
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true));
const response = await Promise.all(requests);
data.push(...response);
}
@@ -230,7 +229,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchProfilesPerChannels');
await operator.batchRecords(models.flat());
}
return {data};
@@ -366,7 +365,7 @@ export async function fetchStatusByIds(serverUrl: string, userIds: string[], fet
user.prepareStatus(status?.status || General.OFFLINE);
}
await operator.batchRecords(users, 'fetchStatusByIds');
await operator.batchRecords(users);
}
}
@@ -531,7 +530,7 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag
}
};
export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => {
export const searchProfiles = async (serverUrl: string, term: string, options: any = {}, fetchOnly = false) => {
let client: Client;
try {
client = NetworkManager.getClient(serverUrl);
@@ -564,7 +563,6 @@ export const searchProfiles = async (serverUrl: string, term: string, options: S
return {data: users};
} catch (error) {
logError('searchProfiles', error);
forceLogoutIfNecessary(serverUrl, error as ClientError);
return {error};
}
@@ -641,7 +639,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
modelsToBatch.push(...models);
}
await operator.batchRecords(modelsToBatch, 'updateAllUsersSince');
await operator.batchRecords(modelsToBatch);
}
} catch {
// Do nothing
@@ -677,7 +675,7 @@ export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecor
}
}
if (models.length && !prepareRecordsOnly) {
serverDatabase.operator.batchRecords(models, 'updateUsersNoLongerVisible');
serverDatabase.operator.batchRecords(models);
}
} catch (error) {
forceLogoutIfNecessary(serverUrl, error as ClientError);
@@ -917,7 +915,7 @@ export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: s
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchTeamAndChannelMembership');
await operator.batchRecords(models.flat());
return {error: undefined};
} catch (error) {
return {error};

View File

@@ -96,7 +96,7 @@ export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: We
c.sortOrder = order.findIndex(findOrder);
});
});
await operator.batchRecords(categories, 'handleCategoryOrderUpdatedEvent');
await operator.batchRecords(categories);
}
} catch (e) {
logError('Category WS: handleCategoryOrderUpdatedEvent', e, msg);

View File

@@ -57,7 +57,7 @@ export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
}
}
}
operator.batchRecords(models, 'handleChannelCreatedEvent');
operator.batchRecords(models);
} catch {
// do nothing
}
@@ -109,7 +109,7 @@ export async function handleChannelUpdatedEvent(serverUrl: string, msg: any) {
if (infoModel.model) {
models.push(...infoModel.model);
}
operator.batchRecords(models, 'handleChannelUpdatedEvent');
operator.batchRecords(models);
} catch {
// Do nothing
}
@@ -165,7 +165,7 @@ export async function handleChannelMemberUpdatedEvent(serverUrl: string, msg: an
if (rolesRequest.roles?.length) {
models.push(...await operator.handleRole({roles: rolesRequest.roles, prepareRecordsOnly: true}));
}
operator.batchRecords(models, 'handleChannelMemberUpdatedEvent');
operator.batchRecords(models);
} catch {
// do nothing
}
@@ -235,7 +235,7 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe
models.push(...userModels);
}
operator.batchRecords(models, 'handleDirectAddedEvent');
operator.batchRecords(models);
} catch {
// do nothing
}
@@ -267,7 +267,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
const prepareModels = await Promise.all(prepare);
const flattenedModels = prepareModels.flat();
if (flattenedModels?.length > 0) {
await operator.batchRecords(flattenedModels, 'handleUserAddedToChannelEvent - prepareMyChannelsForTeam');
await operator.batchRecords(flattenedModels);
}
}
@@ -308,7 +308,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
}
if (models.length) {
await operator.batchRecords(models, 'handleUserAddedToChannelEvent');
await operator.batchRecords(models);
}
await fetchChannelStats(serverUrl, channelId, false);
@@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
}
}
operator.batchRecords(models, 'handleUserRemovedFromChannelEvent');
operator.batchRecords(models);
} catch (error) {
logDebug('cannot handle user removed from channel websocket event', error);
}

View File

@@ -150,7 +150,7 @@ async function doReconnect(serverUrl: string) {
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models, 'doReconnect');
await operator.batchRecords(models);
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
setTeamLoading(serverUrl, false);

View File

@@ -176,7 +176,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
models.push(...postModels);
operator.batchRecords(models, 'handleNewPostEvent');
operator.batchRecords(models);
}
export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage) {
@@ -220,7 +220,7 @@ export async function handlePostEdited(serverUrl: string, msg: WebSocketMessage)
});
models.push(...postModels);
operator.batchRecords(models, 'handlePostEdited');
operator.batchRecords(models);
}
export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage) {
@@ -263,7 +263,7 @@ export async function handlePostDeleted(serverUrl: string, msg: WebSocketMessage
}
if (models.length) {
await operator.batchRecords(models, 'handlePostDeleted');
await operator.batchRecords(models);
}
} catch {
// Do nothing

View File

@@ -107,7 +107,7 @@ async function handleSavePostAdded(serverUrl: string, preferences: PreferenceTyp
return;
}
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST);
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORY_SAVED_POST);
for await (const saved of savedPosts) {
const post = await getPostById(database, saved.name);
if (!post) {

View File

@@ -66,7 +66,7 @@ export async function handleUserRoleUpdatedEvent(serverUrl: string, msg: WebSock
models.push(user);
}
await operator.batchRecords(models, 'handleUserRoleUpdatedEvent');
await operator.batchRecords(models);
}
export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
@@ -118,7 +118,7 @@ export async function handleTeamMemberRoleUpdatedEvent(serverUrl: string, msg: W
});
models.push(...teamMembership);
await operator.batchRecords(models, 'handleTeamMemberRoleUpdatedEvent');
await operator.batchRecords(models);
} catch {
// do nothing
}

View File

@@ -162,5 +162,5 @@ const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDa
}
const models = await Promise.all(modelPromises);
await operator.batchRecords(models.flat(), 'fetchAndStoreJoinedTeamInfo');
await operator.batchRecords(models.flat());
};

View File

@@ -10,7 +10,7 @@ import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import WebsocketManager from '@managers/websocket_manager';
import {queryChannelsByTypes, queryUserChannelsByTypes} from '@queries/servers/channel';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getConfig, getLicense} from '@queries/servers/system';
import {getCurrentUser} from '@queries/servers/user';
import {displayUsername} from '@utils/user';
@@ -71,7 +71,7 @@ export async function handleUserUpdatedEvent(serverUrl: string, msg: WebSocketMe
modelsToBatch.push(...userModel);
try {
await operator.batchRecords(modelsToBatch, 'handleUserUpdatedEvent');
await operator.batchRecords(modelsToBatch);
} catch {
// do nothing
}
@@ -91,7 +91,7 @@ export async function handleUserTypingEvent(serverUrl: string, msg: WebSocketMes
const {users, existingUsers} = await fetchUsersByIds(serverUrl, [msg.data.user_id]);
const user = users?.[0] || existingUsers?.[0];
const namePreference = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
const namePreference = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(namePreference, config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
const currentUser = await getCurrentUser(database);
const username = displayUsername(user, currentUser?.locale, teammateDisplayNameSetting);

View File

@@ -3,15 +3,13 @@
import {buildQueryString} from '@utils/helpers';
import type ClientBase from './base';
export interface ClientAppsMix {
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
}
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => {
const ClientApps = (superclass: any) => class extends superclass {
executeAppCall = async <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean): Promise<AppCallResponse<Res>> => {
const callCopy = {
...call,
context: {

View File

@@ -26,7 +26,6 @@ export default class ClientBase {
requestHeaders: {[x: string]: string} = {};
serverVersion = '';
urlVersion = '/api/v4';
enableLogging = false;
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
this.apiClient = apiClient;
@@ -46,10 +45,6 @@ export default class ClientBase {
}
}
getBaseRoute() {
return this.apiClient.baseUrl || '';
}
getAbsoluteUrl(baseUrl?: string) {
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) {
return baseUrl;
@@ -308,7 +303,7 @@ export default class ClientBase {
}
if (response.ok) {
return returnDataOnly ? (response.data || {}) : response;
return returnDataOnly ? response.data : response;
}
throw new ClientError(this.apiClient.baseUrl, {

View File

@@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientCategoriesMix {
getCategories: (userId: string, teamId: string) => Promise<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
@@ -10,7 +8,7 @@ export interface ClientCategoriesMix {
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
}
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientCategories = (superclass: any) => class extends superclass {
getCategories = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getCategoriesRoute(userId, teamId)}`,

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientChannelsMix {
getAllChannels: (page?: number, perPage?: number, notAssociatedToGroup?: string, excludeDefaultChannels?: boolean, includeTotalCount?: boolean) => Promise<any>;
createChannel: (channel: Channel) => Promise<Channel>;
@@ -42,11 +40,9 @@ export interface ClientChannelsMix {
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchAllChannels: (term: string, teamIds: string[], archivedOnly?: boolean) => Promise<Channel[]>;
updateChannelMemberSchemeRoles: (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => Promise<any>;
getMemberInChannel: (channelId: string, userId: string) => Promise<any>;
}
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientChannels = (superclass: any) => class extends superclass {
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
const queryData = {
page,
@@ -62,7 +58,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
createChannel = async (channel: Channel) => {
this.analytics?.trackAPI('api_channels_create', {team_id: channel.team_id});
this.analytics.trackAPI('api_channels_create', {team_id: channel.team_id});
return this.doFetch(
`${this.getChannelsRoute()}`,
@@ -71,7 +67,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
createDirectChannel = async (userIds: string[]) => {
this.analytics?.trackAPI('api_channels_create_direct');
this.analytics.trackAPI('api_channels_create_direct');
return this.doFetch(
`${this.getChannelsRoute()}/direct`,
@@ -80,7 +76,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
createGroupChannel = async (userIds: string[]) => {
this.analytics?.trackAPI('api_channels_create_group');
this.analytics.trackAPI('api_channels_create_group');
return this.doFetch(
`${this.getChannelsRoute()}/group`,
@@ -89,7 +85,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
deleteChannel = async (channelId: string) => {
this.analytics?.trackAPI('api_channels_delete', {channel_id: channelId});
this.analytics.trackAPI('api_channels_delete', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
@@ -98,7 +94,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
unarchiveChannel = async (channelId: string) => {
this.analytics?.trackAPI('api_channels_unarchive', {channel_id: channelId});
this.analytics.trackAPI('api_channels_unarchive', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/restore`,
@@ -107,7 +103,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
updateChannel = async (channel: Channel) => {
this.analytics?.trackAPI('api_channels_update', {channel_id: channel.id});
this.analytics.trackAPI('api_channels_update', {channel_id: channel.id});
return this.doFetch(
`${this.getChannelRoute(channel.id)}`,
@@ -120,7 +116,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
updateChannelPrivacy = async (channelId: string, privacy: any) => {
this.analytics?.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
this.analytics.trackAPI('api_channels_update_privacy', {channel_id: channelId, privacy});
return this.doFetch(
`${this.getChannelRoute(channelId)}/privacy`,
@@ -129,7 +125,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
patchChannel = async (channelId: string, channelPatch: Partial<Channel>) => {
this.analytics?.trackAPI('api_channels_patch', {channel_id: channelId});
this.analytics.trackAPI('api_channels_patch', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/patch`,
@@ -138,7 +134,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
updateChannelNotifyProps = async (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => {
this.analytics?.trackAPI('api_users_update_channel_notifications', {channel_id: props.channel_id});
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`,
@@ -147,7 +143,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
getChannel = async (channelId: string) => {
this.analytics?.trackAPI('api_channel_get', {channel_id: channelId});
this.analytics.trackAPI('api_channel_get', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}`,
@@ -163,7 +159,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
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});
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}`,
@@ -245,7 +241,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
addToChannel = async (userId: string, channelId: string, postRootId = '') => {
this.analytics?.trackAPI('api_channels_add_member', {channel_id: channelId});
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(
@@ -255,7 +251,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
};
removeFromChannel = async (userId: string, channelId: string) => {
this.analytics?.trackAPI('api_channels_remove_member', {channel_id: channelId});
this.analytics.trackAPI('api_channels_remove_member', {channel_id: channelId});
return this.doFetch(
`${this.getChannelMemberRoute(channelId, userId)}`,
@@ -331,25 +327,6 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
{method: 'post', body},
);
};
// Update a channel member's scheme_admin/scheme_user properties. Typically
// this should either be scheme_admin=false, scheme_user=true for ordinary
// channel member, or scheme_admin=true, scheme_user=true for a channel
// admin.
updateChannelMemberSchemeRoles = (channelId: string, userId: string, isSchemeUser: boolean, isSchemeAdmin: boolean) => {
const body = {scheme_user: isSchemeUser, scheme_admin: isSchemeAdmin};
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/${userId}/schemeRoles`,
{method: 'put', body},
);
};
getMemberInChannel = (channelId: string, userId: string) => {
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/${userId}`,
{method: 'get'},
);
};
};
export default ClientChannels;

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientEmojisMix {
getCustomEmoji: (id: string) => Promise<CustomEmoji>;
getCustomEmojiByName: (name: string) => Promise<CustomEmoji>;
@@ -17,7 +15,7 @@ export interface ClientEmojisMix {
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
}
const ClientEmojis = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientEmojis = (superclass: any) => class extends superclass {
getCustomEmoji = async (id: string) => {
return this.doFetch(
`${this.getEmojisRoute()}/${id}`,

View File

@@ -3,7 +3,6 @@
import {toMilliseconds} from '@utils/datetime';
import type ClientBase from './base';
import type {ClientResponse, ClientResponseError, ProgressPromise, UploadRequestOptions} from '@mattermost/react-native-network-client';
export interface ClientFilesMix {
@@ -23,7 +22,7 @@ export interface ClientFilesMix {
searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise<FileSearchRequest>;
}
const ClientFiles = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientFiles = (superclass: any) => class extends superclass {
getFileUrl(fileId: string, timestamp: number) {
let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}`;
if (timestamp) {
@@ -77,17 +76,13 @@ const ClientFiles = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
},
timeoutInterval: toMilliseconds({minutes: 3}),
};
if (!file.localPath) {
throw new Error('file does not have local path defined');
}
const promise = this.apiClient.upload(url, file.localPath, options) as ProgressPromise<ClientResponse>;
promise.progress!(onProgress).then(onComplete).catch(onError);
return promise.cancel!;
};
searchFilesWithParams = async (teamId: string, params: FileSearchParams) => {
this.analytics?.trackAPI('api_files_search');
this.analytics.trackAPI('api_files_search');
const endpoint = teamId ? `${this.getTeamRoute(teamId)}/files/search` : `${this.getFilesRoute()}/search`;
return this.doFetch(endpoint, {method: 'post', body: params});
};

View File

@@ -6,8 +6,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import ClientError from './error';
import type ClientBase from './base';
type PoliciesResponse<T> = {
policies: T[];
total_count: number;
@@ -27,7 +25,7 @@ export interface ClientGeneralMix {
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
}
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientGeneral = (superclass: any) => class extends superclass {
getOpenGraphMetadata = async (url: string) => {
return this.doFetch(
`${this.urlVersion}/opengraph`,
@@ -51,7 +49,7 @@ const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase)
const url = `${this.urlVersion}/logs`;
if (!this.enableLogging) {
throw new ClientError(this.apiClient.baseUrl, {
throw new ClientError(this.client.baseUrl, {
message: 'Logging disabled.',
url,
});

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientGroupsMix {
getGroup: (id: string) => Promise<Group>;
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
@@ -18,7 +16,7 @@ export interface ClientGroupsMix {
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
}
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientGroups = (superclass: any) => class extends superclass {
getGroup = async (id: string) => {
return this.doFetch(
`${this.urlVersion}/groups/${id}`,

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientIntegrationsMix {
getCommandsList: (teamId: string) => Promise<Command[]>;
getCommandAutocompleteSuggestionsList: (userInput: string, teamId: string, channelId: string, rootId?: string) => Promise<AutocompleteSuggestion[]>;
@@ -16,7 +14,7 @@ export interface ClientIntegrationsMix {
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
}
const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientIntegrations = (superclass: any) => class extends superclass {
getCommandsList = async (teamId: string) => {
return this.doFetch(
`${this.getCommandsRoute()}?team_id=${teamId}`,
@@ -39,7 +37,7 @@ const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: T
};
executeCommand = async (command: string, commandArgs = {}) => {
this.analytics?.trackAPI('api_integrations_used');
this.analytics.trackAPI('api_integrations_used');
return this.doFetch(
`${this.getCommandsRoute()}/execute`,
@@ -48,7 +46,7 @@ const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: T
};
addCommand = async (command: Command) => {
this.analytics?.trackAPI('api_integrations_created');
this.analytics.trackAPI('api_integrations_created');
return this.doFetch(
`${this.getCommandsRoute()}`,
@@ -57,7 +55,7 @@ const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: T
};
submitInteractiveDialog = async (data: DialogSubmission) => {
this.analytics?.trackAPI('api_interactive_messages_dialog_submitted');
this.analytics.trackAPI('api_interactive_messages_dialog_submitted');
return this.doFetch(
`${this.urlVersion}/actions/dialogs/submit`,
{method: 'post', body: data},

View File

@@ -3,13 +3,11 @@
import {General} from '@constants';
import type ClientBase from './base';
export interface ClientNPSMix {
npsGiveFeedbackAction: () => Promise<Post>;
}
const ClientNPS = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientNPS = (superclass: any) => class extends superclass {
npsGiveFeedbackAction = async () => {
return this.doFetch(
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,

View File

@@ -1,13 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientPluginsMix {
getPluginsManifests: () => Promise<ClientPluginManifest[]>;
}
const ClientPlugins = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientPlugins = (superclass: any) => class extends superclass {
getPluginsManifests = async () => {
return this.doFetch(
`${this.getPluginsRoute()}/webapp`,

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientPostsMix {
createPost: (post: Post) => Promise<Post>;
updatePost: (post: Post) => Promise<Post>;
@@ -33,12 +31,12 @@ export interface ClientPostsMix {
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
}
const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientPosts = (superclass: any) => class extends superclass {
createPost = async (post: Post) => {
this.analytics?.trackAPI('api_posts_create', {channel_id: post.channel_id});
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});
this.analytics.trackAPI('api_posts_replied', {channel_id: post.channel_id});
}
return this.doFetch(
@@ -48,7 +46,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
updatePost = async (post: Post) => {
this.analytics?.trackAPI('api_posts_update', {channel_id: post.channel_id});
this.analytics.trackAPI('api_posts_update', {channel_id: post.channel_id});
return this.doFetch(
`${this.getPostRoute(post.id)}`,
@@ -64,7 +62,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
patchPost = async (postPatch: Partial<Post> & {id: string}) => {
this.analytics?.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
this.analytics.trackAPI('api_posts_patch', {channel_id: postPatch.channel_id});
return this.doFetch(
`${this.getPostRoute(postPatch.id)}/patch`,
@@ -73,7 +71,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
deletePost = async (postId: string) => {
this.analytics?.trackAPI('api_posts_delete');
this.analytics.trackAPI('api_posts_delete');
return this.doFetch(
`${this.getPostRoute(postId)}`,
@@ -81,7 +79,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getPostThread = (postId: string, options: FetchPaginatedThreadOptions) => {
getPostThread = (postId: string, options: FetchPaginatedThreadOptions): Promise<PostResponse> => {
const {
fetchThreads = true,
collapsedThreads = false,
@@ -112,7 +110,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getPostsBefore = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => {
this.analytics?.trackAPI('api_posts_get_before', {channel_id: channelId});
this.analytics.trackAPI('api_posts_get_before', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({before: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
@@ -121,7 +119,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getPostsAfter = async (channelId: string, postId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => {
this.analytics?.trackAPI('api_posts_get_after', {channel_id: channelId});
this.analytics.trackAPI('api_posts_get_after', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/posts${buildQueryString({after: postId, page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
@@ -137,7 +135,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getSavedPosts = async (userId: string, channelId = '', teamId = '', page = 0, perPage = PER_PAGE_DEFAULT) => {
this.analytics?.trackAPI('api_posts_get_flagged', {team_id: teamId});
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})}`,
@@ -146,7 +144,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getPinnedPosts = async (channelId: string) => {
this.analytics?.trackAPI('api_posts_get_pinned', {channel_id: channelId});
this.analytics.trackAPI('api_posts_get_pinned', {channel_id: channelId});
return this.doFetch(
`${this.getChannelRoute(channelId)}/pinned`,
{method: 'get'},
@@ -154,7 +152,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
markPostAsUnread = async (userId: string, postId: string) => {
this.analytics?.trackAPI('api_post_set_unread_post');
this.analytics.trackAPI('api_post_set_unread_post');
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
const body = JSON.stringify({collapsed_threads_supported: true});
@@ -166,7 +164,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
pinPost = async (postId: string) => {
this.analytics?.trackAPI('api_posts_pin');
this.analytics.trackAPI('api_posts_pin');
return this.doFetch(
`${this.getPostRoute(postId)}/pin`,
@@ -175,7 +173,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
unpinPost = async (postId: string) => {
this.analytics?.trackAPI('api_posts_unpin');
this.analytics.trackAPI('api_posts_unpin');
return this.doFetch(
`${this.getPostRoute(postId)}/unpin`,
@@ -184,7 +182,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
addReaction = async (userId: string, postId: string, emojiName: string) => {
this.analytics?.trackAPI('api_reactions_save', {post_id: postId});
this.analytics.trackAPI('api_reactions_save', {post_id: postId});
return this.doFetch(
`${this.getReactionsRoute()}`,
@@ -193,7 +191,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
removeReaction = async (userId: string, postId: string, emojiName: string) => {
this.analytics?.trackAPI('api_reactions_delete', {post_id: postId});
this.analytics.trackAPI('api_reactions_delete', {post_id: postId});
return this.doFetch(
`${this.getUserRoute(userId)}/posts/${postId}/reactions/${emojiName}`,
@@ -209,7 +207,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
searchPostsWithParams = async (teamId: string, params: PostSearchParams) => {
this.analytics?.trackAPI('api_posts_search');
this.analytics.trackAPI('api_posts_search');
const endpoint = teamId ? `${this.getTeamRoute(teamId)}/posts/search` : `${this.getPostsRoute()}/search`;
return this.doFetch(endpoint, {method: 'post', body: params});
};
@@ -224,9 +222,9 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
doPostActionWithCookie = async (postId: string, actionId: string, actionCookie: string, selectedOption = '') => {
if (selectedOption) {
this.analytics?.trackAPI('api_interactive_messages_menu_selected');
this.analytics.trackAPI('api_interactive_messages_menu_selected');
} else {
this.analytics?.trackAPI('api_interactive_messages_button_clicked');
this.analytics.trackAPI('api_interactive_messages_button_clicked');
}
const msg: any = {

View File

@@ -1,17 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientPreferencesMix {
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: () => Promise<PreferenceType[]>;
}
const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientPreferences = (superclass: any) => class extends superclass {
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
this.analytics?.trackAPI('action_posts_flag');
this.analytics.trackAPI('action_posts_flag');
return this.doFetch(
`${this.getPreferencesRoute(userId)}`,
{method: 'put', body: preferences},
@@ -26,7 +24,7 @@ const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TB
};
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
this.analytics?.trackAPI('action_posts_unflag');
this.analytics.trackAPI('action_posts_unflag');
return this.doFetch(
`${this.getPreferencesRoute(userId)}/delete`,
{method: 'post', body: preferences},

View File

@@ -5,8 +5,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientTeamsMix {
createTeam: (team: Team) => Promise<Team>;
deleteTeam: (teamId: string) => Promise<any>;
@@ -30,9 +28,9 @@ export interface ClientTeamsMix {
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
}
const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientTeams = (superclass: any) => class extends superclass {
createTeam = async (team: Team) => {
this.analytics?.trackAPI('api_teams_create');
this.analytics.trackAPI('api_teams_create');
return this.doFetch(
`${this.getTeamsRoute()}`,
@@ -41,7 +39,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
deleteTeam = async (teamId: string) => {
this.analytics?.trackAPI('api_teams_delete');
this.analytics.trackAPI('api_teams_delete');
return this.doFetch(
`${this.getTeamRoute(teamId)}`,
@@ -50,7 +48,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
updateTeam = async (team: Team) => {
this.analytics?.trackAPI('api_teams_update_name', {team_id: team.id});
this.analytics.trackAPI('api_teams_update_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}`,
@@ -59,7 +57,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
patchTeam = async (team: Partial<Team> & {id: string}) => {
this.analytics?.trackAPI('api_teams_patch_name', {team_id: team.id});
this.analytics.trackAPI('api_teams_patch_name', {team_id: team.id});
return this.doFetch(
`${this.getTeamRoute(team.id)}/patch`,
@@ -82,7 +80,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getTeamByName = async (teamName: string) => {
this.analytics?.trackAPI('api_teams_get_team_by_name');
this.analytics.trackAPI('api_teams_get_team_by_name');
return this.doFetch(
this.getTeamNameRoute(teamName),
@@ -133,7 +131,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
addToTeam = async (teamId: string, userId: string) => {
this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId});
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
const member = {user_id: userId, team_id: teamId};
return this.doFetch(
@@ -143,7 +141,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
addUsersToTeamGracefully = (teamId: string, userIds: string[]) => {
this.analytics?.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length});
this.analytics.trackAPI('api_teams_batch_add_members', {team_id: teamId, count: userIds.length});
const members: Array<{team_id: string; user_id: string}> = [];
userIds.forEach((id) => members.push({team_id: teamId, user_id: id}));
@@ -155,7 +153,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
sendEmailInvitesToTeamGracefully = (teamId: string, emails: string[]) => {
this.analytics?.trackAPI('api_teams_invite_members', {team_id: teamId});
this.analytics.trackAPI('api_teams_invite_members', {team_id: teamId});
return this.doFetch(
`${this.getTeamRoute(teamId)}/invite/email?graceful=true`,
@@ -172,7 +170,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
removeFromTeam = async (teamId: string, userId: string) => {
this.analytics?.trackAPI('api_teams_remove_members', {team_id: teamId});
this.analytics.trackAPI('api_teams_remove_members', {team_id: teamId});
return this.doFetch(
`${this.getTeamMemberRoute(teamId, userId)}`,

View File

@@ -5,8 +5,6 @@ import {buildQueryString, isMinimumServerVersion} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientThreadsMix {
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => Promise<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
@@ -16,7 +14,7 @@ export interface ClientThreadsMix {
updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise<any>;
}
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientThreads = (superclass: any) => class extends superclass {
getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '') => {
const queryStringObj: Record<string, any> = {
extended: 'true',

View File

@@ -1,14 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type ClientBase from './base';
export interface ClientTosMix {
updateMyTermsOfServiceStatus: (termsOfServiceId: string, accepted: boolean) => Promise<{status: string}>;
getTermsOfService: () => Promise<TermsOfService>;
}
const ClientTos = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientTos = (superclass: any) => class extends superclass {
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
return this.doFetch(
`${this.getUserRoute('me')}/terms_of_service`,

View File

@@ -6,8 +6,6 @@ import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base';
export interface ClientUsersMix {
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>) => Promise<UserProfile>;
@@ -26,7 +24,7 @@ export interface ClientUsersMix {
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, options?: GetUsersOptions) => 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>;
@@ -39,7 +37,7 @@ export interface ClientUsersMix {
getSessions: (userId: string) => Promise<Session[]>;
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
attachDevice: (deviceId: string) => Promise<any>;
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
searchUsers: (term: string, options: any) => Promise<UserProfile[]>;
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
getStatus: (userId: string) => Promise<UserStatus>;
updateStatus: (status: UserStatus) => Promise<UserStatus>;
@@ -48,9 +46,9 @@ export interface ClientUsersMix {
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
}
const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
const ClientUsers = (superclass: any) => class extends superclass {
createUser = async (user: UserProfile, token: string, inviteId: string) => {
this.analytics?.trackAPI('api_users_create');
this.analytics.trackAPI('api_users_create');
const queryParams: any = {};
@@ -76,7 +74,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
patchUser = async (userPatch: Partial<UserProfile> & {id: string}) => {
this.analytics?.trackAPI('api_users_patch');
this.analytics.trackAPI('api_users_patch');
return this.doFetch(
`${this.getUserRoute(userPatch.id)}/patch`,
@@ -85,7 +83,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
updateUser = async (user: UserProfile) => {
this.analytics?.trackAPI('api_users_update');
this.analytics.trackAPI('api_users_update');
return this.doFetch(
`${this.getUserRoute(user.id)}`,
@@ -94,7 +92,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
demoteUserToGuest = async (userId: string) => {
this.analytics?.trackAPI('api_users_demote_user_to_guest');
this.analytics.trackAPI('api_users_demote_user_to_guest');
return this.doFetch(
`${this.getUserRoute(userId)}/demote`,
@@ -103,7 +101,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getKnownUsers = async () => {
this.analytics?.trackAPI('api_get_known_users');
this.analytics.trackAPI('api_get_known_users');
return this.doFetch(
`${this.getUsersRoute()}/known`,
@@ -112,7 +110,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
sendPasswordResetEmail = async (email: string) => {
this.analytics?.trackAPI('api_users_send_password_reset');
this.analytics.trackAPI('api_users_send_password_reset');
return this.doFetch(
`${this.getUsersRoute()}/password/reset/send`,
@@ -121,7 +119,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
setDefaultProfileImage = async (userId: string) => {
this.analytics?.trackAPI('api_users_set_default_profile_picture');
this.analytics.trackAPI('api_users_set_default_profile_picture');
return this.doFetch(
`${this.getUserRoute(userId)}/image`,
@@ -130,10 +128,10 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
login = async (loginId: string, password: string, token = '', deviceId = '', ldapOnly = false) => {
this.analytics?.trackAPI('api_users_login');
this.analytics.trackAPI('api_users_login');
if (ldapOnly) {
this.analytics?.trackAPI('api_users_login_ldap');
this.analytics.trackAPI('api_users_login_ldap');
}
const body: any = {
@@ -161,7 +159,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
loginById = async (id: string, password: string, token = '', deviceId = '') => {
this.analytics?.trackAPI('api_users_login');
this.analytics.trackAPI('api_users_login');
const body: any = {
device_id: deviceId,
id,
@@ -183,7 +181,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
logout = async () => {
this.analytics?.trackAPI('api_users_logout');
this.analytics.trackAPI('api_users_logout');
const response = await this.doFetch(
`${this.getUsersRoute()}/logout`,
@@ -194,7 +192,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfiles = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
this.analytics?.trackAPI('api_profiles_get');
this.analytics.trackAPI('api_profiles_get');
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString({page, per_page: perPage, ...options})}`,
@@ -203,7 +201,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfilesByIds = async (userIds: string[], options = {}) => {
this.analytics?.trackAPI('api_profiles_get_by_ids');
this.analytics.trackAPI('api_profiles_get_by_ids');
return this.doFetch(
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
@@ -212,7 +210,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfilesByUsernames = async (usernames: string[]) => {
this.analytics?.trackAPI('api_profiles_get_by_usernames');
this.analytics.trackAPI('api_profiles_get_by_usernames');
return this.doFetch(
`${this.getUsersRoute()}/usernames`,
@@ -221,7 +219,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfilesInTeam = async (teamId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '', options = {}) => {
this.analytics?.trackAPI('api_profiles_get_in_team', {team_id: teamId, sort});
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})}`,
@@ -230,7 +228,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
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});
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) {
@@ -244,7 +242,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfilesWithoutTeam = async (page = 0, perPage = PER_PAGE_DEFAULT, options = {}) => {
this.analytics?.trackAPI('api_profiles_get_without_team');
this.analytics.trackAPI('api_profiles_get_without_team');
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString({...options, without_team: 1, page, per_page: perPage})}`,
@@ -252,10 +250,10 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => {
this.analytics?.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
const queryStringObj = {in_channel: channelId, ...options};
const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
{method: 'get'},
@@ -263,7 +261,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
getProfilesInGroupChannels = async (channelsIds: string[]) => {
this.analytics?.trackAPI('api_profiles_get_in_group_channels', {channelsIds});
this.analytics.trackAPI('api_profiles_get_in_group_channels', {channelsIds});
return this.doFetch(
`${this.getUsersRoute()}/group_channels`,
@@ -272,7 +270,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
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});
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) {
@@ -374,7 +372,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
};
searchUsers = async (term: string, options: any) => {
this.analytics?.trackAPI('api_search_users');
this.analytics.trackAPI('api_search_users');
return this.doFetch(
`${this.getUsersRoute()}/search`,

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {Preferences} from '@constants';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
import {queryEmojiPreferences} from '@queries/servers/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import EmojiSuggestion from './emoji_suggestion';
@@ -21,9 +21,12 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const isCustomEmojisEnabled = observeConfigBooleanValue(database, 'EnableCustomEmoji');
return {
customEmojis: isCustomEmojisEnabled.pipe(
switchMap((enabled) => (enabled ? queryAllCustomEmojis(database).observe() : of$(emptyEmojiList))),
switchMap((enabled) => (enabled ?
queryAllCustomEmojis(database).observe() :
of$(emptyEmojiList)),
),
),
skinTone: queryEmojiPreferences(database, Preferences.EMOJI_SKINTONE).
skinTone: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
observeWithColumns(['value']).pipe(
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
),

View File

@@ -23,7 +23,7 @@ type Props = {
testID?: string;
}
const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
const intl = useIntl();
const serverUrl = useServerUrl();
const isTablet = useIsTablet();
@@ -183,4 +183,4 @@ const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type
);
};
export default LeaveChannelLabel;
export default LeaveChanelLabel;

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {queryUsersById} from '@queries/servers/user';
import ManageMembersLabel from './manage_members_label';
import type {WithDatabaseArgs} from '@typings/database/database';
type OwnProps = WithDatabaseArgs & {
isDefaultChannel: boolean;
userId: string;
}
const enhanced = withObservables(['isDefaultChannel', 'userId'], ({isDefaultChannel, userId, database}: OwnProps) => {
const users = queryUsersById(database, [userId]).observe();
const canRemoveUser = users.pipe(
switchMap((u) => {
return of$(!isDefaultChannel || (isDefaultChannel && u[0].isGuest));
}),
);
return {
canRemoveUser,
};
});
export default withDatabase(enhanced(ManageMembersLabel));

View File

@@ -1,152 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Alert, DeviceEventEmitter} from 'react-native';
import {fetchChannelStats, removeMemberFromChannel, updateChannelMemberSchemeRoles} from '@actions/remote/channel';
import OptionItem from '@components/option_item';
import {Events, Members} from '@constants';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import {alertErrorWithFallback} from '@utils/draft';
import type {ManageOptionsTypes} from '@constants/members';
const {MAKE_CHANNEL_ADMIN, MAKE_CHANNEL_MEMBER, REMOVE_USER} = Members.ManageOptions;
const messages = defineMessages({
role_change_error: {
id: t('mobile.manage_members.change_role.error'),
defaultMessage: 'An error occurred while trying to update the role. Please check your connection and try again.',
},
make_channel_admin: {
id: t('mobile.manage_members.make_channel_admin'),
defaultMessage: 'Make Channel Admin',
},
make_channel_member: {
id: t('mobile.manage_members.make_channel_member'),
defaultMessage: 'Make Channel Member',
},
remove_title: {
id: t('mobile.manage_members.remove_member'),
defaultMessage: 'Remove From Channel',
},
remove_message: {
id: t('mobile.manage_members.message'),
defaultMessage: 'Are you sure you want to remove the selected member from the channel?',
},
remove_cancel: {
id: t('mobile.manage_members.cancel'),
defaultMessage: 'Cancel',
},
remove_confirm: {
id: t('mobile.manage_members.remove'),
defaultMessage: 'Remove',
},
});
type Props = {
canRemoveUser: boolean;
channelId: string;
manageOption: ManageOptionsTypes;
testID?: string;
userId: string;
}
const ManageMembersLabel = ({canRemoveUser, channelId, manageOption, testID, userId}: Props) => {
const intl = useIntl();
const {formatMessage} = intl;
const serverUrl = useServerUrl();
const handleRemoveUser = useCallback(async () => {
removeMemberFromChannel(serverUrl, channelId, userId);
fetchChannelStats(serverUrl, channelId, false);
await dismissBottomSheet();
DeviceEventEmitter.emit(Events.REMOVE_USER_FROM_CHANNEL, userId);
}, [channelId, serverUrl, userId]);
const removeFromChannel = useCallback(() => {
Alert.alert(
formatMessage(messages.remove_title),
formatMessage(messages.remove_message),
[{
text: formatMessage(messages.remove_cancel),
style: 'cancel',
}, {
text: formatMessage(messages.remove_confirm),
style: 'destructive',
onPress: handleRemoveUser,
}], {cancelable: false},
);
}, [formatMessage, handleRemoveUser]);
const updateChannelMemberSchemeRole = useCallback(async (schemeAdmin: boolean) => {
const result = await updateChannelMemberSchemeRoles(serverUrl, channelId, userId, true, schemeAdmin);
if (result.error) {
alertErrorWithFallback(intl, result.error, messages.role_change_error);
}
await dismissBottomSheet();
DeviceEventEmitter.emit(Events.MANAGE_USER_CHANGE_ROLE, {userId, schemeAdmin});
}, [channelId, userId, intl, serverUrl]);
const onAction = useCallback(() => {
switch (manageOption) {
case REMOVE_USER:
removeFromChannel();
break;
case MAKE_CHANNEL_ADMIN:
updateChannelMemberSchemeRole(true);
break;
case MAKE_CHANNEL_MEMBER:
updateChannelMemberSchemeRole(false);
break;
default:
break;
}
}, [manageOption, removeFromChannel, updateChannelMemberSchemeRole]);
let actionText;
let icon;
let isDestructive = false;
switch (manageOption) {
case REMOVE_USER:
actionText = (formatMessage(messages.remove_title));
icon = 'trash-can-outline';
isDestructive = true;
break;
case MAKE_CHANNEL_ADMIN:
actionText = formatMessage(messages.make_channel_admin);
icon = 'account-outline';
break;
case MAKE_CHANNEL_MEMBER:
actionText = formatMessage(messages.make_channel_member);
icon = 'account-outline';
break;
default:
break;
}
if (manageOption === REMOVE_USER && !canRemoveUser) {
return null;
}
if (!actionText) {
return null;
}
return (
<OptionItem
action={onAction}
destructive={isDestructive}
icon={icon}
label={actionText}
testID={testID}
type='default'
/>
);
};
export default ManageMembersLabel;

View File

@@ -10,7 +10,7 @@ import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannelsWithCalls} from '@calls/state';
import {General} from '@constants';
import {withServerUrl} from '@context/server';
import {observeChannelSettings, observeMyChannel, queryChannelMembers} from '@queries/servers/channel';
import {observeChannelSettings, observeMyChannel} from '@queries/servers/channel';
import {queryDraft} from '@queries/servers/drafts';
import {observeCurrentChannelId, observeCurrentUserId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -67,7 +67,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
let membersCount = of$(0);
if (channel.type === General.GM_CHANNEL) {
membersCount = queryChannelMembers(database, channel.id).observeCount(false);
membersCount = channel.members.observeCount(false);
}
const isUnread = myChannel.pipe(

View File

@@ -6,7 +6,6 @@ import withObservables from '@nozbe/with-observables';
import {combineLatest, of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {observeTeam} from '@queries/servers/team';
@@ -18,7 +17,7 @@ import type TeamModel from '@typings/database/models/servers/team';
const enhanced = withObservables(['post'], ({post, database}: WithDatabaseArgs & { post: PostModel }) => {
const currentTeamId = observeCurrentTeamId(database);
const channel = observeChannel(database, post.id);
const channel = post.channel.observe();
const teamName = combineLatest([channel, currentTeamId]).pipe(
switchMap(([c, tid]) => {

View File

@@ -1,20 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeTeamIdByThread} from '@queries/servers/thread';
import FollowThreadOption from './follow_thread_option';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ThreadModel from '@typings/database/models/servers/thread';
const enhanced = withObservables(['thread'], ({thread, database}: {thread: ThreadModel} & WithDatabaseArgs) => {
const enhanced = withObservables(['thread'], ({thread}: { thread: ThreadModel }) => {
return {
teamId: observeTeamIdByThread(database, thread),
teamId: observeTeamIdByThread(thread),
};
});
export default withDatabase(enhanced(FollowThreadOption));
export default enhanced(FollowThreadOption);

View File

@@ -12,8 +12,9 @@ import {switchMap} from 'rxjs/operators';
import FormattedDate from '@components/formatted_date';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {Preferences} from '@constants';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeCurrentUser} from '@queries/servers/user';
import {getCurrentMomentForTimezone} from '@utils/helpers';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -133,10 +134,10 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
currentUser: observeCurrentUser(database),
isMilitaryTime: queryDisplayNamePreferences(database).
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
observeWithColumns(['value']).pipe(
switchMap(
(preferences) => of$(getDisplayNamePreferenceAsBool(preferences, 'use_military_time')),
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
),
),
}));

View File

@@ -3,11 +3,10 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {of as of$, from as from$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {combineLatest, of as of$, from as from$} from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import {queryFilesForPost} from '@queries/servers/file';
import {observeCanDownloadFiles, observeConfigBooleanValue} from '@queries/servers/system';
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
import {fileExists} from '@utils/file';
import Files from './files';
@@ -37,14 +36,23 @@ const filesLocalPathValidation = async (files: FileModel[], authorId: string) =>
};
const enhance = withObservables(['post'], ({database, post}: EnhanceProps) => {
const enableMobileFileDownload = observeConfigBooleanValue(database, 'EnableMobileFileDownload');
const publicLinkEnabled = observeConfigBooleanValue(database, 'EnablePublicLink');
const filesInfo = queryFilesForPost(database, post.id).observeWithColumns(['local_path']).pipe(
const complianceDisabled = observeLicense(database).pipe(
switchMap((lcs) => of$(lcs?.IsLicensed === 'false' || lcs?.Compliance === 'false')),
);
const canDownloadFiles = combineLatest([enableMobileFileDownload, complianceDisabled]).pipe(
map(([download, compliance]) => compliance || download),
);
const filesInfo = post.files.observeWithColumns(['local_path']).pipe(
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
);
return {
canDownloadFiles: observeCanDownloadFiles(database),
canDownloadFiles,
postId: of$(post.id),
publicLinkEnabled,
filesInfo,

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
import {StyleSheet, Text, useWindowDimensions, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import PlayBack from '@components/files/voice_recording_file/playback';
import {MIC_SIZE, VOICE_MESSAGE_CARD_RATIO} from '@constants/view';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
//i18n
const VOICE_MESSAGE = 'Voice message';
const UPLOADING_TEXT = 'Uploading..(0%)';
type Props = {
file: FileInfo;
uploading: boolean;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
borderWidth: StyleSheet.hairlineWidth,
borderColor: changeOpacity(theme.centerChannelColor, 0.56),
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 6,
shadowOffset: {
width: 0,
height: 3,
},
alignItems: 'center',
},
centerContainer: {
marginLeft: 12,
},
title: {
color: theme.centerChannelColor,
...typography('Heading', 200),
},
uploading: {
color: changeOpacity(theme.centerChannelColor, 0.56),
...typography('Body', 75),
},
close: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.56),
},
mic: {
borderRadius: MIC_SIZE / 2,
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
height: MIC_SIZE,
width: MIC_SIZE,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 12,
},
playBackContainer: {
flexDirection: 'row',
alignItems: 'center',
},
};
});
const VoiceRecordingFile = ({file, uploading}: Props) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const dimensions = useWindowDimensions();
const isVoiceMessage = file.is_voice_recording;
const voiceStyle = useMemo(() => {
return {
width: dimensions.width * VOICE_MESSAGE_CARD_RATIO,
};
}, [dimensions.width]);
const getUploadingView = useCallback(() => {
return (
<>
<View
style={styles.mic}
>
<CompassIcon
name='microphone'
size={24}
color={theme.buttonBg}
/>
</View>
<View style={styles.centerContainer}>
<Text style={styles.title}>{VOICE_MESSAGE}</Text>
<Text style={styles.uploading}>{UPLOADING_TEXT}</Text>
</View>
</>
);
}, [uploading]);
return (
<View
style={[
styles.container,
isVoiceMessage && voiceStyle,
]}
>
{uploading ? getUploadingView() : <PlayBack/>}
</View>
);
};
export default VoiceRecordingFile;

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {TouchableOpacity, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import SoundWave from '@components/post_draft/draft_input/voice_input/sound_wave';
import TimeElapsed from '@components/post_draft/draft_input/voice_input/time_elapsed';
import {MIC_SIZE} from '@constants/view';
import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
mic: {
borderRadius: MIC_SIZE / 2,
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
height: MIC_SIZE,
width: MIC_SIZE,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 12,
},
playBackContainer: {
flexDirection: 'row',
alignItems: 'center',
},
};
});
const PlayBack = () => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const [playing, setPlaying] = useState(false);
const play = preventDoubleTap(() => {
return setPlaying((p) => !p);
});
return (
<View
style={styles.playBackContainer}
>
<TouchableOpacity
style={styles.mic}
onPress={play}
>
<CompassIcon
color={theme.buttonBg}
name='play'
size={24}
/>
</TouchableOpacity>
<SoundWave animating={playing}/>
<TimeElapsed/>
</View>
);
};
export default PlayBack;

View File

@@ -1,19 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useRef} from 'react';
import React, {useCallback, useRef, useState} from 'react';
import {LayoutChangeEvent, Platform, ScrollView, View} from 'react-native';
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
import QuickActions from '@components/post_draft/quick_actions';
import PostPriorityLabel from '@components/post_priority/post_priority_label';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import PostInput from '../post_input';
import QuickActions from '../quick_actions';
import RecordAction from '../record_action';
import SendAction from '../send_action';
import Typing from '../typing';
import Uploads from '../uploads';
import MessageInput from './message_input';
import VoiceInput from './voice_input';
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
@@ -22,6 +24,7 @@ type Props = {
channelId: string;
rootId?: string;
currentUserId: string;
voiceMessageEnabled: boolean;
canShowPostPriority?: boolean;
// Post Props
@@ -51,16 +54,6 @@ const SAFE_AREA_VIEW_EDGES: Edge[] = ['left', 'right'];
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
actionsContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: Platform.select({
ios: 1,
android: 2,
}),
},
inputContainer: {
flex: 1,
flexDirection: 'column',
@@ -84,6 +77,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
},
actionsContainer: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: Platform.select({
ios: 1,
android: 2,
}),
},
sendVoiceMessage: {
position: 'absolute',
right: -5,
top: 16,
},
postPriorityLabel: {
marginLeft: 12,
marginTop: Platform.select({
@@ -95,42 +103,83 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
});
export default function DraftInput({
testID,
addFiles,
canSend,
channelId,
currentUserId,
cursorPosition,
canShowPostPriority,
files,
maxMessageLength,
rootId = '',
value,
uploadFileError,
sendMessage,
canSend,
updateValue,
addFiles,
testID,
updateCursorPosition,
cursorPosition,
updatePostInputTop,
updateValue,
uploadFileError,
value,
voiceMessageEnabled,
postPriority,
updatePostPriority,
setIsFocused,
}: Props) {
const [recording, setRecording] = useState(false);
const theme = useTheme();
const style = getStyleSheet(theme);
const handleLayout = useCallback((e: LayoutChangeEvent) => {
updatePostInputTop(e.nativeEvent.layout.height);
}, []);
const onPresRecording = useCallback(() => {
setRecording(true);
}, []);
const onCloseRecording = useCallback(() => {
setRecording(false);
}, []);
const isHandlingVoice = files[0]?.is_voice_recording || recording;
const inputRef = useRef<PasteInputRef>();
const focus = useCallback(() => {
inputRef.current?.focus();
}, []);
// Render
const postInputTestID = `${testID}.post.input`;
const quickActionsTestID = `${testID}.quick_actions`;
const sendActionTestID = `${testID}.send_action`;
const style = getStyleSheet(theme);
const recordActionTestID = `${testID}.record_action`;
const getActionButton = useCallback(() => {
if (value.length === 0 && files.length === 0 && voiceMessageEnabled) {
return (
<RecordAction
onPress={onPresRecording}
testID={recordActionTestID}
/>
);
}
return (
<SendAction
disabled={!canSend}
sendMessage={sendMessage}
testID={sendActionTestID}
containerStyle={isHandlingVoice && style.sendVoiceMessage}
/>
);
}, [
canSend,
files.length,
onCloseRecording,
onPresRecording,
sendMessage,
testID,
value.length,
voiceMessageEnabled,
isHandlingVoice,
]);
return (
<>
@@ -144,61 +193,63 @@ export default function DraftInput({
style={style.inputWrapper}
testID={testID}
>
<ScrollView
style={style.inputContainer}
contentContainerStyle={style.inputContentContainer}
keyboardShouldPersistTaps={'always'}
scrollEnabled={false}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
pinchGestureEnabled={false}
overScrollMode={'never'}
disableScrollViewPanResponder={true}
keyboardShouldPersistTaps={'always'}
overScrollMode={'never'}
pinchGestureEnabled={false}
scrollEnabled={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
style={style.inputContainer}
>
{Boolean(postPriority?.priority) && (
<View style={style.postPriorityLabel}>
<PostPriorityLabel label={postPriority!.priority}/>
</View>
)}
<PostInput
testID={postInputTestID}
channelId={channelId}
maxMessageLength={maxMessageLength}
rootId={rootId}
cursorPosition={cursorPosition}
updateCursorPosition={updateCursorPosition}
updateValue={updateValue}
value={value}
addFiles={addFiles}
sendMessage={sendMessage}
inputRef={inputRef}
setIsFocused={setIsFocused}
/>
<Uploads
currentUserId={currentUserId}
files={files}
uploadFileError={uploadFileError}
channelId={channelId}
rootId={rootId}
/>
<View style={style.actionsContainer}>
<QuickActions
testID={quickActionsTestID}
fileCount={files.length}
{recording && (
<VoiceInput
addFiles={addFiles}
updateValue={updateValue}
value={value}
postPriority={postPriority}
updatePostPriority={updatePostPriority}
canShowPostPriority={canShowPostPriority}
focus={focus}
onClose={onCloseRecording}
setRecording={setRecording}
/>
<SendAction
testID={sendActionTestID}
disabled={!canSend}
)}
{!recording &&
<MessageInput
addFiles={addFiles}
channelId={channelId}
currentUserId={currentUserId}
cursorPosition={cursorPosition}
files={files}
inputRef={inputRef}
maxMessageLength={maxMessageLength}
rootId={rootId}
sendMessage={sendMessage}
setIsFocused={setIsFocused}
testID={testID}
updateCursorPosition={updateCursorPosition}
updateValue={updateValue}
uploadFileError={uploadFileError}
value={value}
/>
}
<View style={style.actionsContainer}>
{!isHandlingVoice &&
<QuickActions
addFiles={addFiles}
canShowPostPriority={canShowPostPriority}
fileCount={files.length}
postPriority={postPriority}
testID={quickActionsTestID}
updatePostPriority={updatePostPriority}
updateValue={updateValue}
value={value}
focus={focus}
/>
}
{!isHandlingVoice && getActionButton()}
</View>
</ScrollView>
</SafeAreaView>

View File

@@ -4,18 +4,16 @@
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeCurrentUserId} from '@queries/servers/system';
import {observeIsCRTEnabled} from '@queries/servers/thread';
import {observeVoiceMessagesEnabled} from '@queries/servers/system';
import DisplayCRT from './display_crt';
import DraftInput from './draft_input';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
return {
currentUserId: observeCurrentUserId(database),
isCRTEnabled: observeIsCRTEnabled(database),
voiceMessageEnabled: observeVoiceMessagesEnabled(database),
};
});
export default withDatabase(enhanced(DisplayCRT));
export default withDatabase(enhanced(DraftInput));

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import PostInput from '../post_input';
import Uploads from '../uploads';
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
type Props = {
testID?: string;
channelId: string;
rootId?: string;
currentUserId: string;
// Cursor Position Handler
updateCursorPosition: (pos: number) => void;
cursorPosition: number;
// Send Handler
sendMessage: () => void;
maxMessageLength: number;
// Draft Handler
addFiles: (files: FileInfo[]) => void;
files: FileInfo[];
inputRef: React.MutableRefObject<PasteInputRef | undefined>;
setIsFocused: (isFocused: boolean) => void;
uploadFileError: React.ReactNode;
updateValue: (value: string) => void;
value: string;
}
export default function MessageInput({
addFiles,
channelId,
currentUserId,
cursorPosition,
files,
inputRef,
maxMessageLength,
rootId = '',
sendMessage,
setIsFocused,
testID,
updateCursorPosition,
updateValue,
uploadFileError,
value,
}: Props) {
// Render
const postInputTestID = `${testID}.post.input`;
const isHandlingVoice = files[0]?.is_voice_recording;
return (
<>
{!isHandlingVoice && (
<PostInput
addFiles={addFiles}
channelId={channelId}
cursorPosition={cursorPosition}
inputRef={inputRef}
maxMessageLength={maxMessageLength}
rootId={rootId}
sendMessage={sendMessage}
setIsFocused={setIsFocused}
testID={postInputTestID}
updateCursorPosition={updateCursorPosition}
updateValue={updateValue}
value={value}
/>
)}
<Uploads
currentUserId={currentUserId}
files={files}
uploadFileError={uploadFileError}
channelId={channelId}
rootId={rootId}
/>
</>
);
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {View} from 'react-native';
import Animated, {interpolate, SharedValue, useAnimatedStyle, useSharedValue, withRepeat, withTiming} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
import {MIC_SIZE} from '@constants/view';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const iconCommon = {
height: MIC_SIZE,
width: MIC_SIZE,
alignItems: 'center' as const,
justifyContent: 'center' as const,
};
const round = {
borderRadius: MIC_SIZE / 2,
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
};
return {
mic: {
...iconCommon,
...round,
},
abs: {
position: 'absolute',
},
concentric: {
alignItems: 'center',
justifyContent: 'center',
},
};
});
const useConcentricStyles = (circleId: number, sharedValue: SharedValue<number>) => {
const circles = [1.5, 2.5, 3.5];
return useAnimatedStyle(() => {
const scale = interpolate(sharedValue.value, [0, 1], [circles[circleId], 1]);
const opacity = interpolate(sharedValue.value, [0, 1], [1, 0]);
return {
opacity,
transform: [{scale}],
borderRadius: MIC_SIZE / 2,
};
}, [sharedValue]);
};
const AnimatedMicrophone = () => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const val = useSharedValue(0);
const firstCircleAnimx = useConcentricStyles(0, val);
const secondCircleAnimx = useConcentricStyles(1, val);
const thirdCircleAnimx = useConcentricStyles(2, val);
useEffect(() => {
val.value = withRepeat(
withTiming(1, {duration: 1000}),
800,
true,
);
}, []);
return (
<View style={[styles.mic]}>
<View style={styles.concentric} >
<Animated.View style={[styles.mic, styles.abs, firstCircleAnimx]}/>
<Animated.View style={[styles.mic, styles.abs, secondCircleAnimx]}/>
<Animated.View style={[styles.mic, styles.abs, thirdCircleAnimx]}/>
</View>
<View
style={[styles.mic, styles.abs]}
>
<CompassIcon
name='microphone'
size={24}
color={theme.buttonBg}
/>
</View>
</View>
);
};
export default AnimatedMicrophone;

View File

@@ -0,0 +1,109 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {TouchableOpacity, View} from 'react-native';
import CompassIcon from '@components/compass_icon';
import {MIC_SIZE} from '@constants/view';
import {useTheme} from '@context/theme';
import {extractFileInfo} from '@utils/file';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import AnimatedMicrophone from './animated_microphone';
import SoundWave from './sound_wave';
import TimeElapsed from './time_elapsed';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const iconCommon = {
height: MIC_SIZE,
width: MIC_SIZE,
alignItems: 'center' as const,
justifyContent: 'center' as const,
};
const round = {
borderRadius: MIC_SIZE / 2,
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
};
return {
mainContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
height: 88,
},
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
},
mic: {
...iconCommon,
...round,
},
check: {
...iconCommon,
...round,
backgroundColor: theme.buttonBg,
},
close: {
...iconCommon,
},
};
});
type VoiceInputProps = {
setRecording: (v: boolean) => void;
addFiles: (f: FileInfo[]) => void;
onClose: () => void;
}
const VoiceInput = ({onClose, addFiles, setRecording}: VoiceInputProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const record = async () => {
const url = ''; //await recorder.current?.stopRecorder()
const fi = await extractFileInfo([{uri: url}]);
fi[0].is_voice_recording = true;
addFiles(fi as FileInfo[]);
setRecording(false);
};
//todo: to start recording as soon as this screen shows up
// record();
}, []);
return (
<View style={styles.mainContainer}>
<AnimatedMicrophone/>
<SoundWave/>
<TimeElapsed/>
<TouchableOpacity
style={styles.close}
onPress={onClose}
>
<CompassIcon
color={theme.buttonBg}
name='close'
size={24}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.check}
onPress={onClose} // to be fixed when wiring is completed
>
<CompassIcon
color={theme.buttonColor}
name='check'
size={24}
/>
</TouchableOpacity>
</View>
);
};
export default VoiceInput;

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {random} from 'lodash';
import React from 'react';
import {View} from 'react-native';
import Animated, {cancelAnimation, Extrapolation, interpolate, useAnimatedStyle, useSharedValue, withRepeat, withSpring} from 'react-native-reanimated';
import {WAVEFORM_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import useDidUpdate from '@hooks/did_update';
import {makeStyleSheetFromTheme} from '@utils/theme';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
height: WAVEFORM_HEIGHT,
width: 165,
flexDirection: 'row',
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
},
singleBar: {
height: WAVEFORM_HEIGHT,
width: 2,
backgroundColor: theme.buttonBg,
marginRight: 1,
},
};
});
type SoundWaveProps = {
animating?: boolean;
};
const SoundWave = ({animating = true}: SoundWaveProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const animatedValue = useSharedValue(5);
const animatedStyles = useAnimatedStyle(() => {
const newHeight = interpolate(
animatedValue.value,
[5, 40],
[0, 40],
Extrapolation.EXTEND,
);
return {
height: newHeight,
};
}, []);
useDidUpdate(() => {
if (animating) {
animatedValue.value = withRepeat(
withSpring(40, {
damping: 10,
mass: 0.6,
overshootClamping: true,
}),
800,
true,
);
} else {
cancelAnimation(animatedValue);
}
}, [animating]);
const getAudioBars = () => {
const bars = [];
for (let i = 0; i < 50; i++) {
let height;
if (random(i, 50) % 2 === 0) {
height = random(5, 30);
}
bars.push(
<Animated.View
key={i}
style={[
styles.singleBar,
{height},
!height && animatedStyles,
]}
/>,
);
}
return bars;
};
return (
<View style={styles.container}>
{getAudioBars()}
</View>
);
};
export default SoundWave;

View File

@@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {Text} from 'react-native';
//fixme: hook up the time elapsed progress from the lib in here
const TimeElapsed = () => {
const [timeElapsed] = useState('00:00');
return (
<Text>
{timeElapsed}
</Text>
);
};
export default TimeElapsed;

View File

@@ -7,7 +7,7 @@ import React from 'react';
import {of as of$} from 'rxjs';
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
import {observeChannel, observeChannelInfo} from '@queries/servers/channel';
import {observeChannel} from '@queries/servers/channel';
import {observeConfigBooleanValue, observeConfigIntValue} from '@queries/servers/system';
import PostInput from './post_input';
@@ -31,7 +31,7 @@ const enhanced = withObservables(['channelId'], ({database, channelId}: WithData
);
const membersInChannel = channel.pipe(
switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$({memberCount: 0}))),
switchMap((c) => (c ? c.info.observe() : of$({memberCount: 0}))),
switchMap((i: ChannelInfoModel) => of$(i.memberCount)),
distinctUntilChanged(),
);

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {useTheme} from '@context/theme';
type Props = {
onPress: () => void;
testID: string;
}
const styles = {
recordButtonContainer: {
justifyContent: 'flex-end',
paddingRight: 8,
},
recordButton: {
borderRadius: 4,
height: 24,
width: 24,
alignItems: 'center',
justifyContent: 'center',
},
};
function RecordButton({onPress, testID}: Props) {
const theme = useTheme();
return (
<TouchableWithFeedback
onPress={onPress}
style={styles.recordButtonContainer}
testID={testID}
type={'opacity'}
>
<CompassIcon
color={theme.centerChannelColor}
name='microphone'
size={24}
/>
</TouchableWithFeedback>
);
}
export default RecordButton;

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {StyleProp, View, ViewStyle} from 'react-native';
import CompassIcon from '@components/compass_icon';
import TouchableWithFeedback from '@components/touchable_with_feedback';
@@ -13,6 +13,7 @@ type Props = {
testID: string;
disabled: boolean;
sendMessage: () => void;
containerStyle?: StyleProp<ViewStyle>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
@@ -39,6 +40,7 @@ function SendButton({
testID,
disabled,
sendMessage,
containerStyle,
}: Props) {
const theme = useTheme();
const sendButtonTestID = disabled ? `${testID}.send.button.disabled` : `${testID}.send.button`;
@@ -57,7 +59,7 @@ function SendButton({
<TouchableWithFeedback
testID={sendButtonTestID}
onPress={sendMessage}
style={style.sendButtonContainer}
style={[style.sendButtonContainer, containerStyle]}
type={'opacity'}
disabled={disabled}
>

View File

@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
import {observeChannel, observeChannelInfo, observeCurrentChannel} from '@queries/servers/channel';
import {observeChannel, observeCurrentChannel} from '@queries/servers/channel';
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
import {observePermissionForChannel} from '@queries/servers/role';
import {observeConfigBooleanValue, observeConfigIntValue, observeCurrentUserId} from '@queries/servers/system';
@@ -56,7 +56,7 @@ const enhanced = withObservables([], (ownProps: WithDatabaseArgs & OwnProps) =>
}),
);
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
const channelInfo = channel.pipe(switchMap((c) => (c ? c.info.observe() : of$(undefined))));
const membersCount = channelInfo.pipe(
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
);

View File

@@ -12,7 +12,7 @@ import {handleReactionToLatestPost} from '@actions/remote/reactions';
import {setStatus} from '@actions/remote/user';
import {handleCallsSlashCommand} from '@calls/actions/calls';
import {Events, Screens} from '@constants';
import {PostPriorityType} from '@constants/post';
import {PostPriorityType, PostTypes} from '@constants/post';
import {NOTIFY_ALL_MEMBERS} from '@constants/post_draft';
import {useServerUrl} from '@context/server';
import DraftUploadManager from '@managers/draft_upload_manager';
@@ -121,6 +121,7 @@ export default function SendHandler({
channel_id: channelId,
root_id: rootId,
message: value,
type: (files[0]?.is_voice_recording ? PostTypes.VOICE_MESSAGE : '') as PostType,
} as Post;
if (Object.keys(postPriority).length) {

View File

@@ -156,17 +156,14 @@ function Uploads({
{buildFilePreviews()}
</ScrollView>
</Animated.View>
<Animated.View
style={[style.errorContainer, errorAnimatedStyle]}
>
{Boolean(uploadFileError) &&
<View style={style.errorTextContainer}>
<Text style={style.warning}>
{uploadFileError}
</Text>
</View>
}
</Animated.View>

View File

@@ -2,13 +2,15 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native';
import {StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View} from 'react-native';
import Animated from 'react-native-reanimated';
import {updateDraftFile} from '@actions/local/draft';
import FileIcon from '@components/files/file_icon';
import ImageFile from '@components/files/image_file';
import VoiceRecordingFile from '@components/files/voice_recording_file';
import ProgressBar from '@components/progress_bar';
import {VOICE_MESSAGE_CARD_RATIO} from '@constants/view';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useDidUpdate from '@hooks/did_update';
@@ -63,8 +65,10 @@ export default function UploadItem({
const serverUrl = useServerUrl();
const removeCallback = useRef<(() => void)|null>(null);
const [progress, setProgress] = useState(0);
const dimensions = useWindowDimensions();
const loading = DraftUploadManager.isUploading(file.clientId!);
const isVoiceMessage = file.is_voice_recording;
const handlePress = useCallback(() => {
openGallery(file);
@@ -115,6 +119,16 @@ export default function UploadItem({
/>
);
}
if (isVoiceMessage) {
return (
<VoiceRecordingFile
file={file}
uploading={true}
/>
);
}
return (
<FileIcon
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
@@ -124,14 +138,35 @@ export default function UploadItem({
);
}, [file]);
const voiceStyle = useMemo(() => {
return {
width: dimensions.width * VOICE_MESSAGE_CARD_RATIO,
};
}, [dimensions.width]);
return (
<View
key={file.clientId}
style={style.preview}
style={[
style.preview,
isVoiceMessage && voiceStyle,
]}
>
<View style={style.previewContainer}>
<TouchableWithoutFeedback onPress={onGestureEvent}>
<Animated.View style={[styles, style.filePreview]}>
<View
style={[
style.previewContainer,
]}
>
<TouchableWithoutFeedback
onPress={onGestureEvent}
disabled={file.is_voice_recording}
>
<Animated.View
style={[
styles,
style.filePreview,
]}
>
{filePreviewComponent}
</Animated.View>
</TouchableWithoutFeedback>

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {View, Platform} from 'react-native';
import {View, Platform, StyleProp, ViewStyle} from 'react-native';
import {removeDraftFile} from '@actions/local/draft';
import CompassIcon from '@components/compass_icon';
@@ -14,8 +14,9 @@ import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
type Props = {
channelId: string;
rootId: string;
clientId: string;
rootId: string;
containerStyle?: StyleProp<ViewStyle>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
@@ -30,7 +31,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
removeButton: {
borderRadius: 12,
alignSelf: 'center',
// alignSelf: 'center',
marginTop: Platform.select({
ios: 5.4,
android: 4.75,
@@ -44,8 +46,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
export default function UploadRemove({
channelId,
rootId,
containerStyle,
clientId,
rootId,
}: Props) {
const theme = useTheme();
const style = getStyleSheet(theme);
@@ -58,7 +61,7 @@ export default function UploadRemove({
return (
<TouchableWithFeedback
style={style.tappableContainer}
style={[style.tappableContainer, containerStyle]}
onPress={onPress}
type={'opacity'}
>

View File

@@ -6,19 +6,19 @@ import withObservables from '@nozbe/with-observables';
import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentUser} from '@queries/servers/user';
import AddMembers from './add_members';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
const enhance = withObservables(['post'], ({database, post}: WithDatabaseArgs & {post: PostModel}) => ({
currentUser: observeCurrentUser(database),
channelType: observeChannel(database, post.channelId).pipe(
channelType: post.channel.observe().pipe(
switchMap(
(channel) => (channel ? of$(channel.type) : of$(null)),
(channel: ChannelModel) => (channel ? of$(channel.type) : of$(null)),
),
),
}));

View File

@@ -2,8 +2,9 @@
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react';
import {LayoutChangeEvent, StyleProp, View, ViewStyle} from 'react-native';
import {LayoutChangeEvent, StyleProp, Text, View, ViewStyle} from 'react-native';
import {PostTypes} from '@app/constants/post';
import Files from '@components/files';
import FormattedText from '@components/formatted_text';
import JumboEmoji from '@components/jumbo_emoji';
@@ -38,6 +39,7 @@ type BodyProps = {
post: PostModel;
searchPatterns?: SearchPattern[];
showAddReaction?: boolean;
voiceMessageEnabled?: boolean;
theme: Theme;
};
@@ -77,7 +79,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const Body = ({
appsEnabled, hasFiles, hasReactions, highlight, highlightReplyBar,
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAddChannelMember,
location, post, searchPatterns, showAddReaction, theme,
location, post, searchPatterns, showAddReaction, voiceMessageEnabled, theme,
}: BodyProps) => {
const style = getStyleSheet(theme);
const isEdited = postEdited(post);
@@ -159,36 +161,52 @@ const Body = ({
}
if (!hasBeenDeleted) {
body = (
<View style={style.messageBody}>
{message}
{hasContent &&
<Content
isReplyPost={isReplyPost}
layoutWidth={layoutWidth}
location={location}
post={post}
theme={theme}
/>
}
{hasFiles &&
<Files
failed={isFailed}
layoutWidth={layoutWidth}
location={location}
post={post}
isReplyPost={isReplyPost}
/>
}
{hasReactions && showAddReaction &&
<Reactions
location={location}
post={post}
theme={theme}
/>
}
</View>
);
if (voiceMessageEnabled && post.type === PostTypes.VOICE_MESSAGE) {
body = (
<View style={style.messageBody}>
<Text>{'I am a recording'}</Text>
{/* <VoiceMessagePost /> */}
{hasReactions && showAddReaction &&
<Reactions
location={location}
post={post}
theme={theme}
/>
}
</View>
);
} else {
body = (
<View style={style.messageBody}>
{message}
{hasContent &&
<Content
isReplyPost={isReplyPost}
layoutWidth={layoutWidth}
location={location}
post={post}
theme={theme}
/>
}
{hasFiles &&
<Files
failed={isFailed}
layoutWidth={layoutWidth}
location={location}
post={post}
isReplyPost={isReplyPost}
/>
}
{hasReactions && showAddReaction &&
<Reactions
location={location}
post={post}
theme={theme}
/>
}
</View>
);
}
}
return (

View File

@@ -12,7 +12,6 @@ import {handleBindingClick, postEphemeralCallResponseForPost} from '@actions/rem
import {handleGotoLocation} from '@actions/remote/command';
import {AppBindingLocations, AppCallResponseTypes} from '@constants/apps';
import {useServerUrl} from '@context/server';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {showAppForm} from '@screens/navigation';
import {createCallContext} from '@utils/apps';
@@ -23,6 +22,7 @@ import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
import ButtonBindingText from './button_binding_text';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
@@ -135,7 +135,7 @@ const ButtonBinding = ({currentTeamId, binding, post, teamID, theme}: Props) =>
};
const withTeamId = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => ({
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
teamID: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
currentTeamId: observeCurrentTeamId(database),
}));

View File

@@ -10,11 +10,11 @@ import {postEphemeralCallResponseForPost} from '@actions/remote/apps';
import AutocompleteSelector from '@components/autocomplete_selector';
import {useServerUrl} from '@context/server';
import {useAppBinding} from '@hooks/apps';
import {observeChannel} from '@queries/servers/channel';
import {observeCurrentTeamId} from '@queries/servers/system';
import {logDebug} from '@utils/log';
import type {WithDatabaseArgs} from '@typings/database/database';
import type ChannelModel from '@typings/database/models/servers/channel';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
@@ -79,7 +79,7 @@ const MenuBinding = ({binding, currentTeamId, post, teamID}: Props) => {
};
const withTeamId = withObservables(['post'], ({post, database}: {post: PostModel} & WithDatabaseArgs) => ({
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
teamID: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
currentTeamId: observeCurrentTeamId(database),
}));

View File

@@ -6,8 +6,8 @@ import withObservables from '@nozbe/with-observables';
import {of as of$, combineLatest} from 'rxjs';
import {Preferences} from '@constants';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
import {getPreferenceAsBool} from '@helpers/api/preference';
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {observeConfigBooleanValue} from '@queries/servers/system';
import Opengraph from './opengraph';
@@ -22,10 +22,10 @@ const enhance = withObservables(
}
const linkPreviewsConfig = observeConfigBooleanValue(database, 'EnableLinkPreviews');
const linkPreviewPreference = queryDisplayNamePreferences(database, Preferences.LINK_PREVIEW_DISPLAY).
const linkPreviewPreference = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).
observeWithColumns(['value']);
const showLinkPreviews = combineLatest([linkPreviewsConfig, linkPreviewPreference], (cfg, pref) => {
const previewsEnabled = getDisplayNamePreferenceAsBool(pref, Preferences.LINK_PREVIEW_DISPLAY, true);
const previewsEnabled = getPreferenceAsBool(pref, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, true);
return of$(previewsEnabled && cfg);
});

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
import withObservables from '@nozbe/with-observables';
import {observeVoiceMessagesEnabled} from '@queries/servers/system';
import Body from './body';
import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
return {
voiceMessageEnabled: observeVoiceMessagesEnabled(database),
};
});
export default withDatabase(enhanced(Body));

View File

@@ -8,7 +8,6 @@ import {map, switchMap} from 'rxjs/operators';
import {General, Permissions} from '@constants';
import {observeChannel} from '@queries/servers/channel';
import {observeReactionsForPost} from '@queries/servers/reaction';
import {observePermissionForPost} from '@queries/servers/role';
import {observeConfigBooleanValue, observeCurrentUserId} from '@queries/servers/system';
import {observeUser} from '@queries/servers/user';
@@ -43,7 +42,7 @@ const withReactions = withObservables(['post'], ({database, post}: WithReactions
currentUserId,
disabled,
postId: of$(post.id),
reactions: observeReactionsForPost(database, post.id),
reactions: post.reactions.observe(),
};
});

Some files were not shown because too many files have changed in this diff Show More