forked from Ivasoft/mattermost-mobile
Compare commits
31 Commits
voice-mess
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d653c4e19 | ||
|
|
fd25ea163d | ||
|
|
55578a0dce | ||
|
|
a9f325ef43 | ||
|
|
24ee8cc98e | ||
|
|
c27e1116cc | ||
|
|
2aaa366558 | ||
|
|
e72a142974 | ||
|
|
7165830fe0 | ||
|
|
ce5d049a55 | ||
|
|
8d9fab9b53 | ||
|
|
70cf8c5593 | ||
|
|
c9773d031d | ||
|
|
d75b854828 | ||
|
|
f1a06396c6 | ||
|
|
d1cbfe6659 | ||
|
|
ff18feeac4 | ||
|
|
05984b7202 | ||
|
|
511525c9ed | ||
|
|
055c9109ef | ||
|
|
d484a4ff45 | ||
|
|
e6a1cbb2aa | ||
|
|
c77f1dbd6d | ||
|
|
5f349e378e | ||
|
|
78944554a8 | ||
|
|
a44074247e | ||
|
|
27de9d330d | ||
|
|
c1fbaffd3e | ||
|
|
8be99d2c70 | ||
|
|
983d0aab66 | ||
|
|
5160cf9212 |
@@ -114,6 +114,7 @@ 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 }}
|
||||
|
||||
65
NOTICE.txt
65
NOTICE.txt
@@ -91,6 +91,39 @@ 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.
|
||||
@@ -2791,6 +2824,38 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -112,8 +112,8 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 453
|
||||
versionName "2.0.0"
|
||||
versionCode 457
|
||||
versionName "2.1.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
@@ -206,6 +206,10 @@ 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'
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<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"
|
||||
@@ -40,7 +41,6 @@
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -28,7 +27,6 @@ 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;
|
||||
@@ -53,7 +51,11 @@ public class CustomPushNotificationHelper {
|
||||
private static NotificationChannel mHighImportanceChannel;
|
||||
private static NotificationChannel mMinImportanceChannel;
|
||||
|
||||
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
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) {
|
||||
String message = bundle.getString("message", bundle.getString("body"));
|
||||
String senderId = bundle.getString("sender_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
@@ -75,7 +77,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -177,12 +179,12 @@ public class CustomPushNotificationHelper {
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(context, notification, bundle);
|
||||
setNotificationMessagingStyle(context, notification, bundle);
|
||||
setNotificationIcons(notification, bundle);
|
||||
setNotificationMessagingStyle(notification, bundle);
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
|
||||
setNotificationChannel(notification, bundle);
|
||||
setNotificationChannel(context, notification);
|
||||
setNotificationDeleteIntent(context, notification, bundle, notificationId);
|
||||
addNotificationReplyAction(context, notification, bundle, notificationId);
|
||||
|
||||
@@ -254,15 +256,7 @@ public class CustomPushNotificationHelper {
|
||||
return title;
|
||||
}
|
||||
|
||||
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) {
|
||||
private static NotificationCompat.MessagingStyle getMessagingStyle(Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle;
|
||||
final String senderId = "me";
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
@@ -275,7 +269,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
if (serverUrl != null && !type.equals(CustomPushNotificationHelper.PUSH_TYPE_SESSION)) {
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, serverUrl, "me", urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, "me", urlOverride);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
@@ -288,7 +282,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
String conversationTitle = getConversationTitle(bundle);
|
||||
setMessagingStyleConversationTitle(messagingStyle, conversationTitle, bundle);
|
||||
addMessagingStyleMessages(context, messagingStyle, conversationTitle, bundle);
|
||||
addMessagingStyleMessages(messagingStyle, conversationTitle, bundle);
|
||||
|
||||
return messagingStyle;
|
||||
}
|
||||
@@ -315,25 +309,6 @@ 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) {
|
||||
@@ -365,12 +340,15 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
|
||||
// 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());
|
||||
}
|
||||
@@ -386,8 +364,8 @@ public class CustomPushNotificationHelper {
|
||||
notification.setDeleteIntent(deleteIntent);
|
||||
}
|
||||
|
||||
private static void setNotificationMessagingStyle(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(context, bundle);
|
||||
private static void setNotificationMessagingStyle(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = getMessagingStyle(bundle);
|
||||
notification.setStyle(messagingStyle);
|
||||
}
|
||||
|
||||
@@ -400,20 +378,18 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
String smallIcon = bundle.getString("smallIcon");
|
||||
private static void setNotificationIcons(NotificationCompat.Builder notification, Bundle bundle) {
|
||||
String channelName = getConversationTitle(bundle);
|
||||
String senderName = bundle.getString("sender_name");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
String urlOverride = bundle.getString("override_icon_url");
|
||||
|
||||
int smallIconResId = getSmallIconResourceId(context, smallIcon);
|
||||
notification.setSmallIcon(smallIconResId);
|
||||
notification.setSmallIcon(R.mipmap.ic_notification);
|
||||
|
||||
if (serverUrl != null && channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Bitmap avatar = userAvatar(context, serverUrl, senderId, urlOverride);
|
||||
Bitmap avatar = userAvatar(serverUrl, senderId, urlOverride);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
@@ -423,29 +399,31 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
private static Bitmap userAvatar(final String serverUrl, final String userId, final String urlOverride) throws IOException {
|
||||
try {
|
||||
final OkHttpClient client = new OkHttpClient();
|
||||
Request request;
|
||||
String url;
|
||||
Response response;
|
||||
if (!TextUtils.isEmpty(urlOverride)) {
|
||||
request = new Request.Builder().url(urlOverride).build();
|
||||
Request request = new Request.Builder().url(urlOverride).build();
|
||||
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
|
||||
response = client.newCall(request).execute();
|
||||
} else {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
|
||||
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);
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
request = new Request.Builder()
|
||||
.header("Authorization", String.format("Bearer %s", token))
|
||||
.url(url)
|
||||
.build();
|
||||
response = Network.getSync(serverUrl, url, null);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,18 @@ class DatabaseHelper {
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
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
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -38,14 +43,19 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
fun getServerUrlForIdentifier(identifier: String): String? {
|
||||
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
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -63,19 +73,25 @@ class DatabaseHelper {
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
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!!)
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -148,18 +164,23 @@ class DatabaseHelper {
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
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)
|
||||
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
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.mattermost.networkclient.APIClientModule;
|
||||
import com.mattermost.networkclient.enums.RetryTypes;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
||||
public class Network {
|
||||
@@ -35,6 +36,16 @@ 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");
|
||||
|
||||
@@ -145,9 +145,9 @@ public class NotificationHelper {
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
hasMore = bundle.containsKey("root_id") && bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
hasMore = bundle.containsKey("channel_id") && bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -17,7 +16,6 @@ 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;
|
||||
@@ -31,7 +29,6 @@ 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 {
|
||||
@@ -57,27 +54,31 @@ public class CustomPushNotification extends PushNotification {
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
if (ackId != null && serverUrl != null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(String code, String message) {
|
||||
Log.e("ReactNative", code + ": " + message);
|
||||
}
|
||||
});
|
||||
current.putAll(response);
|
||||
mNotificationProps = createProps(current);
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
@@ -118,16 +119,6 @@ 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);
|
||||
@@ -149,10 +140,6 @@ 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());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,6 +43,19 @@ 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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
@@ -42,7 +44,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public static MainApplication instance;
|
||||
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
@@ -69,6 +70,8 @@ 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);
|
||||
}
|
||||
@@ -81,6 +84,7 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,19 +133,6 @@ 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;
|
||||
|
||||
@@ -8,28 +8,16 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
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.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
import com.mattermost.helpers.*;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotificationProps;
|
||||
|
||||
@@ -53,12 +41,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
final int notificationId = intent.getIntExtra(CustomPushNotificationHelper.NOTIFICATION_ID, -1);
|
||||
final String serverUrl = bundle.getString("server_url");
|
||||
if (serverUrl != null) {
|
||||
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
|
||||
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
|
||||
|
||||
if (token != null) {
|
||||
replyToMessage(serverUrl, token, notificationId, message);
|
||||
}
|
||||
replyToMessage(serverUrl, notificationId, message);
|
||||
} else {
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
@@ -67,7 +50,7 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
}
|
||||
}
|
||||
|
||||
protected void replyToMessage(final String serverUrl, final String token, final int notificationId, final CharSequence message) {
|
||||
protected void replyToMessage(final String serverUrl, 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");
|
||||
@@ -75,63 +58,53 @@ public class NotificationReplyBroadcastReceiver extends BroadcastReceiver {
|
||||
rootId = postId;
|
||||
}
|
||||
|
||||
if (token == null || serverUrl == null) {
|
||||
if (serverUrl == null) {
|
||||
onReplyFailed(notificationId);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
String postsEndpoint = "/api/v4/posts?set_online=false";
|
||||
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() {
|
||||
Network.post(serverUrl, postsEndpoint, options, new ResolvePromise() {
|
||||
@Override
|
||||
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", e.getMessage()));
|
||||
public void resolve(@Nullable Object value) {
|
||||
if (value != null) {
|
||||
onReplySuccess(notificationId, message);
|
||||
Log.i("ReactNative", "Reply SUCCESS");
|
||||
} else {
|
||||
Log.i("ReactNative", "Reply FAILED resolved without value");
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reject(Throwable reason) {
|
||||
Log.i("ReactNative", String.format("Reply FAILED exception %s", reason.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",
|
||||
String.format("Reply FAILED status %s BODY %s",
|
||||
response.code(),
|
||||
Objects.requireNonNull(response.body()).string()
|
||||
)
|
||||
);
|
||||
onReplyFailed(notificationId);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
|
||||
@@ -1,135 +1,60 @@
|
||||
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.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.mattermost.helpers.*;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ReceiptDelivery {
|
||||
private static final int[] FIBONACCI_BACKOFF = new int[] { 0, 1, 2, 3, 5, 8 };
|
||||
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"};
|
||||
|
||||
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);
|
||||
public static Bundle send(final String ackId, final String serverUrl, final String postId, final String type, final boolean isIdLoaded) {
|
||||
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with ID-LOADED=%s", ackId, type, serverUrl, isIdLoaded));
|
||||
execute(serverUrl, postId, token, ackId, type, isIdLoaded, promise);
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void makeServerRequest(OkHttpClient client, Request request, Boolean isIdLoaded, int reRequestCount, ResolvePromise promise) {
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
try (Response response = Network.postSync(serverUrl, "api/v4/notifications/ack", options)) {
|
||||
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);
|
||||
return parseAckResponse(jsonResponse);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Bundle parseAckResponse(JSONObject jsonResponse) {
|
||||
try {
|
||||
Bundle bundle = new Bundle();
|
||||
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) {
|
||||
for (String key : ackKeys) {
|
||||
if (jsonResponse.has(key)) {
|
||||
bundle.putString(key, jsonResponse.getString(key));
|
||||
}
|
||||
}
|
||||
promise.resolve(bundle);
|
||||
return bundle;
|
||||
} catch (Exception e) {
|
||||
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());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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.
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ 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) => {
|
||||
@@ -40,7 +39,7 @@ export async function storeCategories(serverUrl: string, categories: CategoryWit
|
||||
}
|
||||
|
||||
if (models.length > 0) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'storeCategories');
|
||||
}
|
||||
|
||||
return {models};
|
||||
@@ -80,7 +79,6 @@ export async function addChannelToDefaultCategory(serverUrl: string, channel: Ch
|
||||
return {error: 'no current user id'};
|
||||
}
|
||||
|
||||
const models: Model[] = [];
|
||||
const categoriesWithChannels: CategoryWithChannels[] = [];
|
||||
|
||||
if (isDMorGM(channel)) {
|
||||
@@ -100,13 +98,12 @@ 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);
|
||||
await operator.batchRecords(models, 'addChannelToDefaultCategory');
|
||||
}
|
||||
|
||||
return {models};
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryDisplayNamePreferences} 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);
|
||||
await operator.batchRecords(models, 'switchToChannel');
|
||||
}
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'removeCurrentUserFromChannel');
|
||||
}
|
||||
}
|
||||
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]);
|
||||
await operator.batchRecords([model], 'setChannelDeleteAt');
|
||||
} 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]);
|
||||
await operator.batchRecords([member], 'markChannelAsViewed');
|
||||
}
|
||||
|
||||
return {member};
|
||||
@@ -206,7 +206,7 @@ export async function markChannelAsUnread(serverUrl: string, channelId: string,
|
||||
m.isUnread = true;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
await operator.batchRecords([member], 'markChannelAsUnread');
|
||||
}
|
||||
|
||||
return {member};
|
||||
@@ -226,7 +226,7 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
|
||||
member.prepareUpdate((m) => {
|
||||
m.messageCount = 0;
|
||||
});
|
||||
await operator.batchRecords([member]);
|
||||
await operator.batchRecords([member], 'resetMessageCount');
|
||||
|
||||
return member;
|
||||
} catch (error) {
|
||||
@@ -254,7 +254,7 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
|
||||
}
|
||||
|
||||
if (flattenedModels.length) {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
@@ -273,7 +273,7 @@ export async function updateMyChannelFromWebsocket(serverUrl: string, channelMem
|
||||
m.roles = channelMember.roles;
|
||||
});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords([member]);
|
||||
operator.batchRecords([member], 'updateMyChannelFromWebsocket');
|
||||
}
|
||||
}
|
||||
return {model: member};
|
||||
@@ -293,7 +293,7 @@ export async function updateChannelInfoFromChannel(serverUrl: string, channel: C
|
||||
}],
|
||||
prepareRecordsOnly: true});
|
||||
if (!prepareRecordsOnly) {
|
||||
operator.batchRecords(newInfo);
|
||||
operator.batchRecords(newInfo, 'updateChannelInfoFromChannel');
|
||||
}
|
||||
return {model: newInfo};
|
||||
} catch (error) {
|
||||
@@ -317,7 +317,7 @@ export async function updateLastPostAt(serverUrl: string, channelId: string, las
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel]);
|
||||
await operator.batchRecords([myChannel], 'updateLastPostAt');
|
||||
}
|
||||
|
||||
return {member: myChannel};
|
||||
@@ -345,7 +345,7 @@ export async function updateMyChannelLastFetchedAt(serverUrl: string, channelId:
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([myChannel]);
|
||||
await operator.batchRecords([myChannel], 'updateMyChannelLastFetchedAt');
|
||||
}
|
||||
|
||||
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 queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryDisplayNamePreferences(database, 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);
|
||||
await operator.batchRecords(models, 'updateChannelsDisplayName');
|
||||
}
|
||||
|
||||
return {models};
|
||||
|
||||
@@ -18,18 +18,15 @@ 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]);
|
||||
await operator.batchRecords([draft], 'updateDraftFile');
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -61,7 +58,7 @@ export async function removeDraftFile(serverUrl: string, channelId: string, root
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
await operator.batchRecords([draft], 'removeDraftFile');
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -102,7 +99,7 @@ export async function updateDraftMessage(serverUrl: string, channelId: string, r
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
await operator.batchRecords([draft], 'updateDraftMessage');
|
||||
}
|
||||
|
||||
return {draft};
|
||||
@@ -132,7 +129,7 @@ export async function addFilesToDraft(serverUrl: string, channelId: string, root
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([draft]);
|
||||
await operator.batchRecords([draft], 'addFilesToDraft');
|
||||
}
|
||||
|
||||
return {draft};
|
||||
|
||||
@@ -127,14 +127,14 @@ export async function removePost(serverUrl: string, post: PostModel | Post) {
|
||||
}
|
||||
|
||||
if (removeModels.length) {
|
||||
await operator.batchRecords(removeModels);
|
||||
await operator.batchRecords(removeModels, 'removePost (combined user activity)');
|
||||
}
|
||||
} else {
|
||||
const postModel = await getPostById(database, post.id);
|
||||
if (postModel) {
|
||||
const preparedPost = await prepareDeletePost(postModel);
|
||||
if (preparedPost.length) {
|
||||
await operator.batchRecords(preparedPost);
|
||||
await operator.batchRecords(preparedPost, 'removePost');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export async function markPostAsDeleted(serverUrl: string, post: Post, prepareRe
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([dbPost]);
|
||||
await operator.batchRecords([dbPost], 'markPostAsDeleted');
|
||||
}
|
||||
return {model};
|
||||
} catch (error) {
|
||||
@@ -229,7 +229,7 @@ export async function storePostsForChannel(
|
||||
}
|
||||
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'storePostsForChannel');
|
||||
}
|
||||
|
||||
return {models};
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function removeUserFromTeam(serverUrl: string, teamId: string) {
|
||||
models.push(...system);
|
||||
}
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'removeUserFromTeam');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function addSearchToTeamSearchHistory(serverUrl: string, teamId: st
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'addSearchToTeamHistory');
|
||||
return {searchModel};
|
||||
} catch (error) {
|
||||
logError('Failed addSearchToTeamSearchHistory', error);
|
||||
|
||||
@@ -40,7 +40,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
|
||||
models.push(...history);
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'switchToGlobalThreads');
|
||||
}
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'switchToThread');
|
||||
}
|
||||
} 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);
|
||||
await operator.batchRecords(models, 'switchToThread');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export async function createThreadFromNewPost(serverUrl: string, post: Post, pre
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'createThreadFromNewPost');
|
||||
}
|
||||
|
||||
return {models};
|
||||
@@ -257,7 +257,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
|
||||
}
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'processReceivedThreads');
|
||||
}
|
||||
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);
|
||||
await operator.batchRecords(models, 'markTeamThreadsAsRead');
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
@@ -300,7 +300,7 @@ export async function markThreadAsViewed(serverUrl: string, threadId: string, pr
|
||||
});
|
||||
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([thread]);
|
||||
await operator.batchRecords([thread], 'markThreadAsViewed');
|
||||
}
|
||||
|
||||
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]);
|
||||
await operator.batchRecords([model], 'updateThread');
|
||||
}
|
||||
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);
|
||||
await operator.batchRecords(models, 'updateTeamThreadsSync');
|
||||
}
|
||||
return {models};
|
||||
} catch (error) {
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function setCurrentUserStatusOffline(serverUrl: string) {
|
||||
}
|
||||
|
||||
user.prepareStatus(General.OFFLINE);
|
||||
await operator.batchRecords([user]);
|
||||
await operator.batchRecords([user], 'setCurrentUserStatusOffline');
|
||||
return null;
|
||||
} catch (error) {
|
||||
logError('Failed setCurrentUserStatusOffline', error);
|
||||
@@ -54,7 +54,7 @@ export async function updateLocalCustomStatus(serverUrl: string, user: UserModel
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'updateLocalCustomStatus');
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
@@ -97,27 +97,37 @@ 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);
|
||||
const user = await getCurrentUser(database);
|
||||
|
||||
let user: UserModel | undefined;
|
||||
|
||||
if (userId) {
|
||||
user = await getUserById(database, userId);
|
||||
} else {
|
||||
user = await getCurrentUser(database);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const u = user;
|
||||
await database.write(async () => {
|
||||
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;
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -15,8 +16,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} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareMyChannelsForTeam, getChannelById, getChannelByName, getMyChannel, getChannelInfo, queryMyChannelSettingsByIds, getMembersCountByChannelsId, deleteChannelMembership, queryChannelsById} from '@queries/servers/channel';
|
||||
import {queryDisplayNamePreferences} 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';
|
||||
@@ -35,9 +36,10 @@ import {setDirectChannelVisible} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {addCurrentUserToTeam, fetchTeamByName, removeCurrentUserFromTeam} from './team';
|
||||
import {fetchProfilesInGroupChannels, fetchProfilesPerChannels, fetchUsersByIds, updateUsersNoLongerVisible} from './user';
|
||||
import {fetchProfilesInChannel, 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';
|
||||
@@ -49,6 +51,99 @@ 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) {
|
||||
@@ -79,7 +174,7 @@ export async function addMembersToChannel(serverUrl: string, channelId: string,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'addMembersToChannel');
|
||||
}
|
||||
return {channelMemberships};
|
||||
} catch (error) {
|
||||
@@ -156,7 +251,7 @@ export async function createChannel(serverUrl: string, displayName: string, purp
|
||||
models.push(...categoriesModels.models);
|
||||
}
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'createChannel');
|
||||
}
|
||||
fetchChannelStats(serverUrl, channelData.id, false);
|
||||
EphemeralStore.creatingChannel = false;
|
||||
@@ -201,7 +296,7 @@ export async function patchChannel(serverUrl: string, channelPatch: Partial<Chan
|
||||
models.push(channel);
|
||||
}
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'patchChannel');
|
||||
}
|
||||
return {channel: channelData};
|
||||
} catch (error) {
|
||||
@@ -248,7 +343,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
|
||||
models.push(...removeUserModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'leaveChannel');
|
||||
|
||||
if (isTabletDevice) {
|
||||
switchToLastChannel(serverUrl);
|
||||
@@ -298,7 +393,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
|
||||
}));
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchChannelCreator');
|
||||
}
|
||||
|
||||
return {user};
|
||||
@@ -389,7 +484,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);
|
||||
await operator.batchRecords(models, 'fetchMyChannelsForTeam');
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
@@ -438,38 +533,37 @@ 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) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
const users: UserProfile[] = [];
|
||||
const updatedChannels = new Set<Channel>();
|
||||
|
||||
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);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
gms.push(c);
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
const users: UserProfile[] = [];
|
||||
const updatedChannels = new Set<Channel>();
|
||||
|
||||
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]));
|
||||
|
||||
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;
|
||||
}
|
||||
gms.push(c);
|
||||
}
|
||||
|
||||
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),
|
||||
@@ -515,7 +609,7 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchMissingDirectChannelInfo');
|
||||
}
|
||||
|
||||
return {directChannels: updatedChannelsArray, users};
|
||||
@@ -530,7 +624,7 @@ export async function fetchDirectChannelsInfo(serverUrl: string, directChannels:
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).fetch();
|
||||
const preferences = await queryDisplayNamePreferences(database).fetch();
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences, config?.LockTeammateNameDisplay, config?.TeammateNameDisplay, license);
|
||||
@@ -593,7 +687,7 @@ export async function joinChannel(serverUrl: string, teamId: string, channelId?:
|
||||
}
|
||||
if (flattenedModels?.length > 0) {
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
await operator.batchRecords(flattenedModels, 'joinChannel');
|
||||
} catch {
|
||||
logError('FAILED TO BATCH CHANNELS');
|
||||
}
|
||||
@@ -776,7 +870,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
if (displayName) {
|
||||
created.display_name = displayName;
|
||||
} else {
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
@@ -819,7 +913,7 @@ export async function createDirectChannel(serverUrl: string, userId: string, dis
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'createDirectChannel');
|
||||
EphemeralStore.creatingDMorGMTeammates = [];
|
||||
fetchRolesIfNeeded(serverUrl, member.roles.split(' '));
|
||||
return {data: created};
|
||||
@@ -918,7 +1012,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
return {data: created};
|
||||
}
|
||||
|
||||
const preferences = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const preferences = await queryDisplayNamePreferences(database, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
@@ -953,7 +1047,7 @@ export async function createGroupChannel(serverUrl: string, userIds: string[]) {
|
||||
}
|
||||
|
||||
models.push(...userModels);
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models, 'createGroupChannel');
|
||||
}
|
||||
}
|
||||
EphemeralStore.creatingDMorGMTeammates = [];
|
||||
@@ -1298,7 +1392,7 @@ export const convertChannelToPrivate = async (serverUrl: string, channelId: stri
|
||||
channel.prepareUpdate((c) => {
|
||||
c.type = General.PRIVATE_CHANNEL;
|
||||
});
|
||||
await operator.batchRecords([channel]);
|
||||
await operator.batchRecords([channel], 'convertChannelToPrivate');
|
||||
}
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
|
||||
@@ -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);
|
||||
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - 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);
|
||||
await operator.batchRecords(me, 'appEntry - upgrade store me');
|
||||
}
|
||||
}
|
||||
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
|
||||
|
||||
const dt = Date.now();
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'appEntry');
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
|
||||
@@ -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(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
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);
|
||||
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
operator.batchRecords(models, 'deferredAppEntryActions');
|
||||
|
||||
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(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
initialTeamId = selectDefaultTeam(teamData.teams, meData.user.locale, teamOrderPreference, config.ExperimentalPrimaryTeam)?.id || '';
|
||||
}
|
||||
const gqlRoles = [
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs)
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'loginEntry');
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {Preferences, Screens} from '@constants';
|
||||
import {Screens} from '@constants';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryThemePreferences} 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 queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, teamId).fetch();
|
||||
const themes = await queryThemePreferences(database, 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);
|
||||
await operator.batchRecords(models, 'pushNotificationEntry');
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {DOWNLOAD_TIMEOUT} from '@constants/network';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
@@ -32,10 +33,11 @@ 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) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupChannels]);
|
||||
await operator.batchRecords([...groups, ...groupChannels], 'fetchGroupsForChannel');
|
||||
}
|
||||
|
||||
return {groups, groupChannels};
|
||||
@@ -110,7 +110,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupTeams]);
|
||||
await operator.batchRecords([...groups, ...groupTeams], 'fetchGroupsForTeam');
|
||||
}
|
||||
|
||||
return {groups, groupTeams};
|
||||
@@ -136,7 +136,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe
|
||||
]);
|
||||
|
||||
if (!fetchOnly) {
|
||||
await operator.batchRecords([...groups, ...groupMemberships]);
|
||||
await operator.batchRecords([...groups, ...groupMemberships], 'fetchGroupsForMember');
|
||||
}
|
||||
|
||||
return {groups, groupMemberships};
|
||||
|
||||
@@ -128,7 +128,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
initialPostModels.push(...reactionModels);
|
||||
}
|
||||
|
||||
await operator.batchRecords(initialPostModels);
|
||||
await operator.batchRecords(initialPostModels, 'createPost - initial');
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'createPost - failure');
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
@@ -192,7 +192,7 @@ export async function createPost(serverUrl: string, post: Partial<Post>, files:
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'createPost - success');
|
||||
|
||||
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]);
|
||||
await operator.batchRecords([post], 'retryFailedPost - first update');
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'retryFailedPost - success update');
|
||||
} 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]);
|
||||
await operator.batchRecords([post], 'retryFailedPost - error update');
|
||||
}
|
||||
|
||||
return {error};
|
||||
@@ -375,7 +375,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0,
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchPosts');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -434,7 +434,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchPostsBefore');
|
||||
} 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);
|
||||
await operator.batchRecords(models, 'fetchPostsSince');
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -621,7 +621,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchPostThread');
|
||||
}
|
||||
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);
|
||||
await operator.batchRecords(models, 'fetchPostsAround');
|
||||
}
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'fetchMissingChannelsFromPosts');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -809,7 +809,7 @@ export async function fetchPostById(serverUrl: string, postId: string, fetchOnly
|
||||
}
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchPostById');
|
||||
}
|
||||
|
||||
return {post};
|
||||
@@ -1036,7 +1036,7 @@ export async function fetchSavedPosts(serverUrl: string, teamId?: string, channe
|
||||
return mdls;
|
||||
});
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchSavedPosts');
|
||||
|
||||
return {
|
||||
order,
|
||||
@@ -1118,7 +1118,7 @@ export async function fetchPinnedPosts(serverUrl: string, channelId: string) {
|
||||
return mdls;
|
||||
});
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'fetchPinnedPosts');
|
||||
|
||||
return {
|
||||
order,
|
||||
|
||||
@@ -5,14 +5,12 @@ import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {querySavedPostsPreferences} 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;
|
||||
@@ -56,7 +54,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
|
||||
try {
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
const favPref: PreferenceType = {
|
||||
category: CATEGORY_FAVORITE_CHANNEL,
|
||||
category: Preferences.CATEGORIES.FAVORITE_CHANNEL,
|
||||
name: channelId,
|
||||
user_id: userId,
|
||||
value: String(isFavorite),
|
||||
@@ -77,7 +75,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
|
||||
const userId = await getCurrentUserId(operator.database);
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
category: CATEGORY_SAVED_POST,
|
||||
category: Preferences.CATEGORIES.SAVED_POST,
|
||||
name: postId,
|
||||
value: 'true',
|
||||
};
|
||||
@@ -116,23 +114,15 @@ 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 {
|
||||
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 {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
const records = await querySavedPostsPreferences(database, postId).fetch();
|
||||
const postPreferenceRecord = records.find((r) => postId === r.name);
|
||||
const pref = {
|
||||
user_id: userId,
|
||||
category: CATEGORY_SAVED_POST,
|
||||
category: Preferences.CATEGORIES.SAVED_POST,
|
||||
name: postId,
|
||||
value: 'true',
|
||||
};
|
||||
@@ -161,7 +151,8 @@ 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 category = channel.type === General.DM_CHANNEL ? CATEGORY_DIRECT_CHANNEL_SHOW : CATEGORY_GROUP_CHANNEL_SHOW;
|
||||
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
|
||||
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
|
||||
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channelId;
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
@@ -185,7 +176,7 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
|
||||
const userId = await getCurrentUserId(database);
|
||||
const pref: PreferenceType = {
|
||||
user_id: userId,
|
||||
category: Preferences.CATEGORY_EMOJI,
|
||||
category: Preferences.CATEGORIES.EMOJI,
|
||||
name: Preferences.EMOJI_SKINTONE,
|
||||
value: skinCode,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function addReaction(serverUrl: string, postId: string, emojiName:
|
||||
models.push(...recent);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'addReaction');
|
||||
|
||||
return {reaction};
|
||||
}
|
||||
|
||||
@@ -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, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareMyPreferences, queryDisplayNamePreferences} 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(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences!, Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
const teamRoles: string[] = [];
|
||||
const teamMembers = new Set<string>();
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function retryInitialTeamAndChannel(serverUrl: string) {
|
||||
),
|
||||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'retryInitialTeamAndChannel');
|
||||
|
||||
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 queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const prefs = await queryDisplayNamePreferences(database, 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);
|
||||
await operator.batchRecords(models, 'retryInitialChannel');
|
||||
|
||||
const directChannels = chData!.channels!.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
|
||||
@@ -103,7 +103,7 @@ export const searchPosts = async (serverUrl: string, teamId: string, params: Pos
|
||||
return mdls;
|
||||
});
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'searchPosts');
|
||||
return {
|
||||
order,
|
||||
posts: postsArray,
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
|
||||
prepareCategoriesAndCategoriesChannels(operator, categories || [], true),
|
||||
])).flat();
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'addUserToTeam');
|
||||
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);
|
||||
await operator.batchRecords(flattenedModels, 'fetchMyTeams');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
await operator.batchRecords(flattenedModels, 'fetchMyTeam');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
await operator.batchRecords(models, 'fetchTeamByName');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,7 +441,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'handleTeamChange');
|
||||
}
|
||||
DeviceEventEmitter.emit(Events.TEAM_SWITCH, false);
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export async function updateTermsOfServiceStatus(serverUrl: string, id: string,
|
||||
u.termsOfServiceId = '';
|
||||
}
|
||||
});
|
||||
operator.batchRecords([currentUser]);
|
||||
operator.batchRecords([currentUser], 'updateTermsOfServiceStatus');
|
||||
}
|
||||
return {resp};
|
||||
} catch (error) {
|
||||
|
||||
@@ -360,7 +360,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
|
||||
const allNewThreads = await fetchThreads(
|
||||
serverUrl,
|
||||
teamId,
|
||||
{deleted: true, since: syncData.latest},
|
||||
{deleted: true, since: syncData.latest + 1},
|
||||
);
|
||||
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);
|
||||
await operator.batchRecords(models, 'syncTeamThreads');
|
||||
} 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);
|
||||
await operator.batchRecords(models, 'loadEarlierThreads');
|
||||
} catch (err) {
|
||||
if (__DEV__) {
|
||||
throw err;
|
||||
|
||||
@@ -74,7 +74,7 @@ export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyU
|
||||
}
|
||||
};
|
||||
|
||||
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, fetchOnly = false): Promise<ProfilesInChannelRequest> {
|
||||
export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, 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);
|
||||
const users = await client.getProfilesInChannel(channelId, options);
|
||||
const uniqueUsers = Array.from(new Set(users));
|
||||
const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId);
|
||||
if (!fetchOnly) {
|
||||
@@ -102,12 +102,13 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin
|
||||
modelPromises.push(prepare);
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchProfilesInChannel');
|
||||
}
|
||||
}
|
||||
|
||||
return {channelId, users: filteredUsers};
|
||||
} catch (error) {
|
||||
logError('fetchProfilesInChannel', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {channelId, error};
|
||||
}
|
||||
@@ -177,7 +178,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchProfilesInGroupChannels');
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -198,7 +199,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, true));
|
||||
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true));
|
||||
const response = await Promise.all(requests);
|
||||
data.push(...response);
|
||||
}
|
||||
@@ -229,7 +230,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchProfilesPerChannels');
|
||||
}
|
||||
|
||||
return {data};
|
||||
@@ -365,7 +366,7 @@ export async function fetchStatusByIds(serverUrl: string, userIds: string[], fet
|
||||
user.prepareStatus(status?.status || General.OFFLINE);
|
||||
}
|
||||
|
||||
await operator.batchRecords(users);
|
||||
await operator.batchRecords(users, 'fetchStatusByIds');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +531,7 @@ export const fetchProfilesInTeam = async (serverUrl: string, teamId: string, pag
|
||||
}
|
||||
};
|
||||
|
||||
export const searchProfiles = async (serverUrl: string, term: string, options: any = {}, fetchOnly = false) => {
|
||||
export const searchProfiles = async (serverUrl: string, term: string, options: SearchUserOptions, fetchOnly = false) => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -563,6 +564,7 @@ export const searchProfiles = async (serverUrl: string, term: string, options: a
|
||||
|
||||
return {data: users};
|
||||
} catch (error) {
|
||||
logError('searchProfiles', error);
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
return {error};
|
||||
}
|
||||
@@ -639,7 +641,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc
|
||||
modelsToBatch.push(...models);
|
||||
}
|
||||
|
||||
await operator.batchRecords(modelsToBatch);
|
||||
await operator.batchRecords(modelsToBatch, 'updateAllUsersSince');
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
@@ -675,7 +677,7 @@ export async function updateUsersNoLongerVisible(serverUrl: string, prepareRecor
|
||||
}
|
||||
}
|
||||
if (models.length && !prepareRecordsOnly) {
|
||||
serverDatabase.operator.batchRecords(models);
|
||||
serverDatabase.operator.batchRecords(models, 'updateUsersNoLongerVisible');
|
||||
}
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error as ClientError);
|
||||
@@ -915,7 +917,7 @@ export const fetchTeamAndChannelMembership = async (serverUrl: string, userId: s
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchTeamAndChannelMembership');
|
||||
return {error: undefined};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -96,7 +96,7 @@ export async function handleCategoryOrderUpdatedEvent(serverUrl: string, msg: We
|
||||
c.sortOrder = order.findIndex(findOrder);
|
||||
});
|
||||
});
|
||||
await operator.batchRecords(categories);
|
||||
await operator.batchRecords(categories, 'handleCategoryOrderUpdatedEvent');
|
||||
}
|
||||
} catch (e) {
|
||||
logError('Category WS: handleCategoryOrderUpdatedEvent', e, msg);
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function handleChannelCreatedEvent(serverUrl: string, msg: any) {
|
||||
}
|
||||
}
|
||||
}
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models, 'handleChannelCreatedEvent');
|
||||
} 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);
|
||||
operator.batchRecords(models, 'handleChannelUpdatedEvent');
|
||||
} 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);
|
||||
operator.batchRecords(models, 'handleChannelMemberUpdatedEvent');
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
@@ -235,7 +235,7 @@ export async function handleDirectAddedEvent(serverUrl: string, msg: WebSocketMe
|
||||
models.push(...userModels);
|
||||
}
|
||||
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models, 'handleDirectAddedEvent');
|
||||
} 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);
|
||||
await operator.batchRecords(flattenedModels, 'handleUserAddedToChannelEvent - prepareMyChannelsForTeam');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'handleUserAddedToChannelEvent');
|
||||
}
|
||||
|
||||
await fetchChannelStats(serverUrl, channelId, false);
|
||||
@@ -356,7 +356,7 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
||||
}
|
||||
}
|
||||
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models, 'handleUserRemovedFromChannelEvent');
|
||||
} catch (error) {
|
||||
logDebug('cannot handle user removed from channel websocket event', error);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
await operator.batchRecords(models, 'doReconnect');
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
|
||||
models.push(...postModels);
|
||||
|
||||
operator.batchRecords(models);
|
||||
operator.batchRecords(models, 'handleNewPostEvent');
|
||||
}
|
||||
|
||||
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);
|
||||
operator.batchRecords(models, 'handlePostEdited');
|
||||
}
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'handlePostDeleted');
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
|
||||
@@ -107,7 +107,7 @@ async function handleSavePostAdded(serverUrl: string, preferences: PreferenceTyp
|
||||
return;
|
||||
}
|
||||
|
||||
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORY_SAVED_POST);
|
||||
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST);
|
||||
for await (const saved of savedPosts) {
|
||||
const post = await getPostById(database, saved.name);
|
||||
if (!post) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function handleUserRoleUpdatedEvent(serverUrl: string, msg: WebSock
|
||||
models.push(user);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models);
|
||||
await operator.batchRecords(models, 'handleUserRoleUpdatedEvent');
|
||||
}
|
||||
|
||||
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);
|
||||
await operator.batchRecords(models, 'handleTeamMemberRoleUpdatedEvent');
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@@ -162,5 +162,5 @@ const fetchAndStoreJoinedTeamInfo = async (serverUrl: string, operator: ServerDa
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
await operator.batchRecords(models.flat(), 'fetchAndStoreJoinedTeamInfo');
|
||||
};
|
||||
|
||||
@@ -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 {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryDisplayNamePreferences} 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);
|
||||
await operator.batchRecords(modelsToBatch, 'handleUserUpdatedEvent');
|
||||
} 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 queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT).fetch();
|
||||
const namePreference = await queryDisplayNamePreferences(database, 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);
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
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 = (superclass: any) => class extends superclass {
|
||||
executeAppCall = async <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean): Promise<AppCallResponse<Res>> => {
|
||||
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
executeAppCall = async (call: AppCallRequest, trackAsSubmit: boolean) => {
|
||||
const callCopy = {
|
||||
...call,
|
||||
context: {
|
||||
|
||||
@@ -26,6 +26,7 @@ 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;
|
||||
@@ -45,6 +46,10 @@ export default class ClientBase {
|
||||
}
|
||||
}
|
||||
|
||||
getBaseRoute() {
|
||||
return this.apiClient.baseUrl || '';
|
||||
}
|
||||
|
||||
getAbsoluteUrl(baseUrl?: string) {
|
||||
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) {
|
||||
return baseUrl;
|
||||
@@ -303,7 +308,7 @@ export default class ClientBase {
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return returnDataOnly ? response.data : response;
|
||||
return returnDataOnly ? (response.data || {}) : response;
|
||||
}
|
||||
|
||||
throw new ClientError(this.apiClient.baseUrl, {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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[]>;
|
||||
@@ -8,7 +10,7 @@ export interface ClientCategoriesMix {
|
||||
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
|
||||
}
|
||||
|
||||
const ClientCategories = (superclass: any) => class extends superclass {
|
||||
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getCategories = async (userId: string, teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCategoriesRoute(userId, teamId)}`,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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>;
|
||||
@@ -40,9 +42,11 @@ 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 = (superclass: any) => class extends superclass {
|
||||
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getAllChannels = async (page = 0, perPage = PER_PAGE_DEFAULT, notAssociatedToGroup = '', excludeDefaultChannels = false, includeTotalCount = false) => {
|
||||
const queryData = {
|
||||
page,
|
||||
@@ -58,7 +62,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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()}`,
|
||||
@@ -67,7 +71,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
createDirectChannel = async (userIds: string[]) => {
|
||||
this.analytics.trackAPI('api_channels_create_direct');
|
||||
this.analytics?.trackAPI('api_channels_create_direct');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/direct`,
|
||||
@@ -76,7 +80,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
createGroupChannel = async (userIds: string[]) => {
|
||||
this.analytics.trackAPI('api_channels_create_group');
|
||||
this.analytics?.trackAPI('api_channels_create_group');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}/group`,
|
||||
@@ -85,7 +89,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -94,7 +98,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -103,7 +107,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -116,7 +120,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -125,7 +129,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -134,7 +138,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -143,7 +147,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -159,7 +163,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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}`,
|
||||
@@ -241,7 +245,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -251,7 +255,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -327,6 +331,25 @@ const ClientChannels = (superclass: any) => class extends superclass {
|
||||
{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;
|
||||
|
||||
@@ -5,6 +5,8 @@ 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>;
|
||||
@@ -15,7 +17,7 @@ export interface ClientEmojisMix {
|
||||
autocompleteCustomEmoji: (name: string) => Promise<CustomEmoji[]>;
|
||||
}
|
||||
|
||||
const ClientEmojis = (superclass: any) => class extends superclass {
|
||||
const ClientEmojis = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getCustomEmoji = async (id: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getEmojisRoute()}/${id}`,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
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 {
|
||||
@@ -22,7 +23,7 @@ export interface ClientFilesMix {
|
||||
searchFilesWithParams: (teamId: string, FileSearchParams: string) => Promise<FileSearchRequest>;
|
||||
}
|
||||
|
||||
const ClientFiles = (superclass: any) => class extends superclass {
|
||||
const ClientFiles = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getFileUrl(fileId: string, timestamp: number) {
|
||||
let url = `${this.apiClient.baseUrl}${this.getFileRoute(fileId)}`;
|
||||
if (timestamp) {
|
||||
@@ -76,13 +77,17 @@ const ClientFiles = (superclass: any) => class extends superclass {
|
||||
},
|
||||
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});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@ 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;
|
||||
@@ -25,7 +27,7 @@ export interface ClientGeneralMix {
|
||||
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
|
||||
}
|
||||
|
||||
const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getOpenGraphMetadata = async (url: string) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/opengraph`,
|
||||
@@ -49,7 +51,7 @@ const ClientGeneral = (superclass: any) => class extends superclass {
|
||||
const url = `${this.urlVersion}/logs`;
|
||||
|
||||
if (!this.enableLogging) {
|
||||
throw new ClientError(this.client.baseUrl, {
|
||||
throw new ClientError(this.apiClient.baseUrl, {
|
||||
message: 'Logging disabled.',
|
||||
url,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ 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[]>;
|
||||
@@ -16,7 +18,7 @@ export interface ClientGroupsMix {
|
||||
getAllTeamsAssociatedToGroup: (groupId: string, filterAllowReference?: boolean) => Promise<{groupTeams: GroupTeam[]}>;
|
||||
}
|
||||
|
||||
const ClientGroups = (superclass: any) => class extends superclass {
|
||||
const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getGroup = async (id: string) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/groups/${id}`,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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[]>;
|
||||
@@ -14,7 +16,7 @@ export interface ClientIntegrationsMix {
|
||||
submitInteractiveDialog: (data: DialogSubmission) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
const ClientIntegrations = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getCommandsList = async (teamId: string) => {
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}?team_id=${teamId}`,
|
||||
@@ -37,7 +39,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
executeCommand = async (command: string, commandArgs = {}) => {
|
||||
this.analytics.trackAPI('api_integrations_used');
|
||||
this.analytics?.trackAPI('api_integrations_used');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}/execute`,
|
||||
@@ -46,7 +48,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
addCommand = async (command: Command) => {
|
||||
this.analytics.trackAPI('api_integrations_created');
|
||||
this.analytics?.trackAPI('api_integrations_created');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getCommandsRoute()}`,
|
||||
@@ -55,7 +57,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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},
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
import type ClientBase from './base';
|
||||
|
||||
export interface ClientNPSMix {
|
||||
npsGiveFeedbackAction: () => Promise<Post>;
|
||||
}
|
||||
|
||||
const ClientNPS = (superclass: any) => class extends superclass {
|
||||
const ClientNPS = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
npsGiveFeedbackAction = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPluginRoute(General.NPS_PLUGIN_ID)}/api/v1/give_feedback`,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// 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 = (superclass: any) => class extends superclass {
|
||||
const ClientPlugins = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getPluginsManifests = async () => {
|
||||
return this.doFetch(
|
||||
`${this.getPluginsRoute()}/webapp`,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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>;
|
||||
@@ -31,12 +33,12 @@ export interface ClientPostsMix {
|
||||
doPostActionWithCookie: (postId: string, actionId: string, actionCookie: string, selectedOption?: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientPosts = (superclass: any) => class extends superclass {
|
||||
const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) => 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(
|
||||
@@ -46,7 +48,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -62,7 +64,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -71,7 +73,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
deletePost = async (postId: string) => {
|
||||
this.analytics.trackAPI('api_posts_delete');
|
||||
this.analytics?.trackAPI('api_posts_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
@@ -79,7 +81,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
getPostThread = (postId: string, options: FetchPaginatedThreadOptions): Promise<PostResponse> => {
|
||||
getPostThread = (postId: string, options: FetchPaginatedThreadOptions) => {
|
||||
const {
|
||||
fetchThreads = true,
|
||||
collapsedThreads = false,
|
||||
@@ -110,7 +112,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -119,7 +121,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -135,7 +137,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -144,7 +146,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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'},
|
||||
@@ -152,7 +154,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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});
|
||||
@@ -164,7 +166,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
pinPost = async (postId: string) => {
|
||||
this.analytics.trackAPI('api_posts_pin');
|
||||
this.analytics?.trackAPI('api_posts_pin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/pin`,
|
||||
@@ -173,7 +175,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
unpinPost = async (postId: string) => {
|
||||
this.analytics.trackAPI('api_posts_unpin');
|
||||
this.analytics?.trackAPI('api_posts_unpin');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}/unpin`,
|
||||
@@ -182,7 +184,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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()}`,
|
||||
@@ -191,7 +193,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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}`,
|
||||
@@ -207,7 +209,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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});
|
||||
};
|
||||
@@ -222,9 +224,9 @@ const ClientPosts = (superclass: any) => class extends superclass {
|
||||
|
||||
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 = {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// 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 = (superclass: any) => class extends superclass {
|
||||
const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TBase) => 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},
|
||||
@@ -24,7 +26,7 @@ const ClientPreferences = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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},
|
||||
|
||||
@@ -5,6 +5,8 @@ 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>;
|
||||
@@ -28,9 +30,9 @@ export interface ClientTeamsMix {
|
||||
getTeamIconUrl: (teamId: string, lastTeamIconUpdate: number) => string;
|
||||
}
|
||||
|
||||
const ClientTeams = (superclass: any) => class extends superclass {
|
||||
const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
createTeam = async (team: Team) => {
|
||||
this.analytics.trackAPI('api_teams_create');
|
||||
this.analytics?.trackAPI('api_teams_create');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}`,
|
||||
@@ -39,7 +41,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
deleteTeam = async (teamId: string) => {
|
||||
this.analytics.trackAPI('api_teams_delete');
|
||||
this.analytics?.trackAPI('api_teams_delete');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getTeamRoute(teamId)}`,
|
||||
@@ -48,7 +50,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -57,7 +59,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -80,7 +82,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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),
|
||||
@@ -131,7 +133,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -141,7 +143,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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}));
|
||||
@@ -153,7 +155,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -170,7 +172,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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>;
|
||||
@@ -14,7 +16,7 @@ export interface ClientThreadsMix {
|
||||
updateThreadFollow: (userId: string, teamId: string, threadId: string, state: boolean) => Promise<any>;
|
||||
}
|
||||
|
||||
const ClientThreads = (superclass: any) => class extends superclass {
|
||||
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => 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',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// 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 = (superclass: any) => class extends superclass {
|
||||
const ClientTos = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/terms_of_service`,
|
||||
|
||||
@@ -6,6 +6,8 @@ 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>;
|
||||
@@ -24,7 +26,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, page?: number, perPage?: number, sort?: string) => Promise<UserProfile[]>;
|
||||
getProfilesInChannel: (channelId: string, options?: GetUsersOptions) => 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>;
|
||||
@@ -37,7 +39,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: any) => Promise<UserProfile[]>;
|
||||
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
|
||||
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
|
||||
getStatus: (userId: string) => Promise<UserStatus>;
|
||||
updateStatus: (status: UserStatus) => Promise<UserStatus>;
|
||||
@@ -46,9 +48,9 @@ export interface ClientUsersMix {
|
||||
removeRecentCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
}
|
||||
|
||||
const ClientUsers = (superclass: any) => class extends superclass {
|
||||
const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) => 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 = {};
|
||||
|
||||
@@ -74,7 +76,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -83,7 +85,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
updateUser = async (user: UserProfile) => {
|
||||
this.analytics.trackAPI('api_users_update');
|
||||
this.analytics?.trackAPI('api_users_update');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(user.id)}`,
|
||||
@@ -92,7 +94,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -101,7 +103,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
getKnownUsers = async () => {
|
||||
this.analytics.trackAPI('api_get_known_users');
|
||||
this.analytics?.trackAPI('api_get_known_users');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/known`,
|
||||
@@ -110,7 +112,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -119,7 +121,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -128,10 +130,10 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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 = {
|
||||
@@ -159,7 +161,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -181,7 +183,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
logout = async () => {
|
||||
this.analytics.trackAPI('api_users_logout');
|
||||
this.analytics?.trackAPI('api_users_logout');
|
||||
|
||||
const response = await this.doFetch(
|
||||
`${this.getUsersRoute()}/logout`,
|
||||
@@ -192,7 +194,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -201,7 +203,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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)}`,
|
||||
@@ -210,7 +212,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -219,7 +221,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -228,7 +230,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -242,7 +244,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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})}`,
|
||||
@@ -250,10 +252,10 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
|
||||
this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
|
||||
getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => {
|
||||
this.analytics?.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
|
||||
|
||||
const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
|
||||
const queryStringObj = {in_channel: channelId, ...options};
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
|
||||
{method: 'get'},
|
||||
@@ -261,7 +263,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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`,
|
||||
@@ -270,7 +272,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
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) {
|
||||
@@ -372,7 +374,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
|
||||
};
|
||||
|
||||
searchUsers = async (term: string, options: any) => {
|
||||
this.analytics.trackAPI('api_search_users');
|
||||
this.analytics?.trackAPI('api_search_users');
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/search`,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {queryEmojiPreferences} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
|
||||
import EmojiSuggestion from './emoji_suggestion';
|
||||
@@ -21,12 +21,9 @@ 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: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_EMOJI, Preferences.EMOJI_SKINTONE).
|
||||
skinTone: queryEmojiPreferences(database, Preferences.EMOJI_SKINTONE).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((prefs) => of$(prefs?.[0]?.value ?? 'default')),
|
||||
),
|
||||
|
||||
@@ -23,7 +23,7 @@ type Props = {
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
|
||||
const LeaveChannelLabel = ({canLeave, channelId, displayName, isOptionItem, type, testID}: Props) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const isTablet = useIsTablet();
|
||||
@@ -183,4 +183,4 @@ const LeaveChanelLabel = ({canLeave, channelId, displayName, isOptionItem, type,
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveChanelLabel;
|
||||
export default LeaveChannelLabel;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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));
|
||||
@@ -0,0 +1,152 @@
|
||||
// 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;
|
||||
@@ -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} from '@queries/servers/channel';
|
||||
import {observeChannelSettings, observeMyChannel, queryChannelMembers} 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 = channel.members.observeCount(false);
|
||||
membersCount = queryChannelMembers(database, channel.id).observeCount(false);
|
||||
}
|
||||
|
||||
const isUnread = myChannel.pipe(
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
|
||||
@@ -17,7 +18,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 = post.channel.observe();
|
||||
const channel = observeChannel(database, post.id);
|
||||
|
||||
const teamName = combineLatest([channel, currentTeamId]).pipe(
|
||||
switchMap(([c, tid]) => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// 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}: { thread: ThreadModel }) => {
|
||||
const enhanced = withObservables(['thread'], ({thread, database}: {thread: ThreadModel} & WithDatabaseArgs) => {
|
||||
return {
|
||||
teamId: observeTeamIdByThread(thread),
|
||||
teamId: observeTeamIdByThread(database, thread),
|
||||
};
|
||||
});
|
||||
|
||||
export default enhanced(FollowThreadOption);
|
||||
export default withDatabase(enhanced(FollowThreadOption));
|
||||
|
||||
@@ -12,9 +12,8 @@ import {switchMap} from 'rxjs/operators';
|
||||
import FormattedDate from '@components/formatted_date';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import FormattedTime from '@components/formatted_time';
|
||||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryDisplayNamePreferences} from '@queries/servers/preference';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {getCurrentMomentForTimezone} from '@utils/helpers';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -134,10 +133,10 @@ const CustomStatusExpiry = ({currentUser, isMilitaryTime, showPrefix, showTimeCo
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUser: observeCurrentUser(database),
|
||||
isMilitaryTime: queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS).
|
||||
isMilitaryTime: queryDisplayNamePreferences(database).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap(
|
||||
(preferences) => of$(getPreferenceAsBool(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time', false)),
|
||||
(preferences) => of$(getDisplayNamePreferenceAsBool(preferences, 'use_military_time')),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {combineLatest, of as of$, from as from$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
import {of as of$, from as from$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeConfigBooleanValue, observeLicense} from '@queries/servers/system';
|
||||
import {queryFilesForPost} from '@queries/servers/file';
|
||||
import {observeCanDownloadFiles, observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {fileExists} from '@utils/file';
|
||||
|
||||
import Files from './files';
|
||||
@@ -36,23 +37,14 @@ 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 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(
|
||||
const filesInfo = queryFilesForPost(database, post.id).observeWithColumns(['local_path']).pipe(
|
||||
switchMap((fs) => from$(filesLocalPathValidation(fs, post.userId))),
|
||||
);
|
||||
|
||||
return {
|
||||
canDownloadFiles,
|
||||
canDownloadFiles: observeCanDownloadFiles(database),
|
||||
postId: of$(post.id),
|
||||
publicLinkEnabled,
|
||||
filesInfo,
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,61 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,19 +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 {observeVoiceMessagesEnabled} from '@queries/servers/system';
|
||||
|
||||
import DraftInput from './draft_input';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
return {
|
||||
voiceMessageEnabled: observeVoiceMessagesEnabled(database),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(enhanced(DraftInput));
|
||||
@@ -1,21 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import React, {useCallback, useRef} 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 RecordAction from '../record_action';
|
||||
import PostInput from '../post_input';
|
||||
import QuickActions from '../quick_actions';
|
||||
import SendAction from '../send_action';
|
||||
import Typing from '../typing';
|
||||
|
||||
import MessageInput from './message_input';
|
||||
import VoiceInput from './voice_input';
|
||||
import Uploads from '../uploads';
|
||||
|
||||
import type {PasteInputRef} from '@mattermost/react-native-paste-input';
|
||||
|
||||
@@ -24,7 +22,6 @@ type Props = {
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
currentUserId: string;
|
||||
voiceMessageEnabled: boolean;
|
||||
canShowPostPriority?: boolean;
|
||||
|
||||
// Post Props
|
||||
@@ -54,6 +51,16 @@ 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',
|
||||
@@ -77,21 +84,6 @@ 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({
|
||||
@@ -103,83 +95,42 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
});
|
||||
|
||||
export default function DraftInput({
|
||||
addFiles,
|
||||
canSend,
|
||||
testID,
|
||||
channelId,
|
||||
currentUserId,
|
||||
cursorPosition,
|
||||
canShowPostPriority,
|
||||
files,
|
||||
maxMessageLength,
|
||||
rootId = '',
|
||||
sendMessage,
|
||||
testID,
|
||||
updateCursorPosition,
|
||||
updatePostInputTop,
|
||||
updateValue,
|
||||
uploadFileError,
|
||||
value,
|
||||
voiceMessageEnabled,
|
||||
uploadFileError,
|
||||
sendMessage,
|
||||
canSend,
|
||||
updateValue,
|
||||
addFiles,
|
||||
updateCursorPosition,
|
||||
cursorPosition,
|
||||
updatePostInputTop,
|
||||
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 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,
|
||||
]);
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -193,63 +144,61 @@ export default function DraftInput({
|
||||
style={style.inputWrapper}
|
||||
testID={testID}
|
||||
>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
disableScrollViewPanResponder={true}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
overScrollMode={'never'}
|
||||
pinchGestureEnabled={false}
|
||||
scrollEnabled={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={style.inputContainer}
|
||||
contentContainerStyle={style.inputContentContainer}
|
||||
keyboardShouldPersistTaps={'always'}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
pinchGestureEnabled={false}
|
||||
overScrollMode={'never'}
|
||||
disableScrollViewPanResponder={true}
|
||||
>
|
||||
{Boolean(postPriority?.priority) && (
|
||||
<View style={style.postPriorityLabel}>
|
||||
<PostPriorityLabel label={postPriority!.priority}/>
|
||||
</View>
|
||||
)}
|
||||
{recording && (
|
||||
<VoiceInput
|
||||
addFiles={addFiles}
|
||||
onClose={onCloseRecording}
|
||||
setRecording={setRecording}
|
||||
/>
|
||||
)}
|
||||
{!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}
|
||||
/>
|
||||
}
|
||||
<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}>
|
||||
{!isHandlingVoice &&
|
||||
<QuickActions
|
||||
addFiles={addFiles}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
fileCount={files.length}
|
||||
postPriority={postPriority}
|
||||
testID={quickActionsTestID}
|
||||
updatePostPriority={updatePostPriority}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
focus={focus}
|
||||
/>
|
||||
}
|
||||
{!isHandlingVoice && getActionButton()}
|
||||
<QuickActions
|
||||
testID={quickActionsTestID}
|
||||
fileCount={files.length}
|
||||
addFiles={addFiles}
|
||||
updateValue={updateValue}
|
||||
value={value}
|
||||
postPriority={postPriority}
|
||||
updatePostPriority={updatePostPriority}
|
||||
canShowPostPriority={canShowPostPriority}
|
||||
focus={focus}
|
||||
/>
|
||||
<SendAction
|
||||
testID={sendActionTestID}
|
||||
disabled={!canSend}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
@@ -1,83 +0,0 @@
|
||||
// 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// 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;
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,99 +0,0 @@
|
||||
// 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;
|
||||
@@ -1,18 +0,0 @@
|
||||
// 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;
|
||||
@@ -7,7 +7,7 @@ import React from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {observeChannel} from '@queries/servers/channel';
|
||||
import {observeChannel, observeChannelInfo} 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 ? c.info.observe() : of$({memberCount: 0}))),
|
||||
switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$({memberCount: 0}))),
|
||||
switchMap((i: ChannelInfoModel) => of$(i.memberCount)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// 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;
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {StyleProp, View, ViewStyle} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
@@ -13,7 +13,6 @@ type Props = {
|
||||
testID: string;
|
||||
disabled: boolean;
|
||||
sendMessage: () => void;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
@@ -40,7 +39,6 @@ function SendButton({
|
||||
testID,
|
||||
disabled,
|
||||
sendMessage,
|
||||
containerStyle,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const sendButtonTestID = disabled ? `${testID}.send.button.disabled` : `${testID}.send.button`;
|
||||
@@ -59,7 +57,7 @@ function SendButton({
|
||||
<TouchableWithFeedback
|
||||
testID={sendButtonTestID}
|
||||
onPress={sendMessage}
|
||||
style={[style.sendButtonContainer, containerStyle]}
|
||||
style={style.sendButtonContainer}
|
||||
type={'opacity'}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -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, observeCurrentChannel} from '@queries/servers/channel';
|
||||
import {observeChannel, observeChannelInfo, 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 ? c.info.observe() : of$(undefined))));
|
||||
const channelInfo = channel.pipe(switchMap((c) => (c ? observeChannelInfo(database, c.id) : of$(undefined))));
|
||||
const membersCount = channelInfo.pipe(
|
||||
switchMap((i) => (i ? of$(i.memberCount) : of$(0))),
|
||||
);
|
||||
|
||||
@@ -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, PostTypes} from '@constants/post';
|
||||
import {PostPriorityType} from '@constants/post';
|
||||
import {NOTIFY_ALL_MEMBERS} from '@constants/post_draft';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DraftUploadManager from '@managers/draft_upload_manager';
|
||||
@@ -121,7 +121,6 @@ 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) {
|
||||
|
||||
@@ -156,14 +156,17 @@ 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>
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {StyleSheet, TouchableWithoutFeedback, useWindowDimensions, View} from 'react-native';
|
||||
import {StyleSheet, TouchableWithoutFeedback, 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';
|
||||
@@ -65,10 +63,8 @@ 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);
|
||||
@@ -119,16 +115,6 @@ export default function UploadItem({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isVoiceMessage) {
|
||||
return (
|
||||
<VoiceRecordingFile
|
||||
file={file}
|
||||
uploading={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileIcon
|
||||
backgroundColor={changeOpacity(theme.centerChannelColor, 0.08)}
|
||||
@@ -138,35 +124,14 @@ 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,
|
||||
isVoiceMessage && voiceStyle,
|
||||
]}
|
||||
style={style.preview}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
style.previewContainer,
|
||||
]}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={onGestureEvent}
|
||||
disabled={file.is_voice_recording}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles,
|
||||
style.filePreview,
|
||||
]}
|
||||
>
|
||||
<View style={style.previewContainer}>
|
||||
<TouchableWithoutFeedback onPress={onGestureEvent}>
|
||||
<Animated.View style={[styles, style.filePreview]}>
|
||||
{filePreviewComponent}
|
||||
</Animated.View>
|
||||
</TouchableWithoutFeedback>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View, Platform, StyleProp, ViewStyle} from 'react-native';
|
||||
import {View, Platform} from 'react-native';
|
||||
|
||||
import {removeDraftFile} from '@actions/local/draft';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -14,9 +14,8 @@ import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
clientId: string;
|
||||
rootId: string;
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
@@ -31,8 +30,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
removeButton: {
|
||||
borderRadius: 12,
|
||||
|
||||
// alignSelf: 'center',
|
||||
alignSelf: 'center',
|
||||
marginTop: Platform.select({
|
||||
ios: 5.4,
|
||||
android: 4.75,
|
||||
@@ -46,9 +44,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
||||
export default function UploadRemove({
|
||||
channelId,
|
||||
containerStyle,
|
||||
clientId,
|
||||
rootId,
|
||||
clientId,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -61,7 +58,7 @@ export default function UploadRemove({
|
||||
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={[style.tappableContainer, containerStyle]}
|
||||
style={style.tappableContainer}
|
||||
onPress={onPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
|
||||
@@ -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: post.channel.observe().pipe(
|
||||
channelType: observeChannel(database, post.channelId).pipe(
|
||||
switchMap(
|
||||
(channel: ChannelModel) => (channel ? of$(channel.type) : of$(null)),
|
||||
(channel) => (channel ? of$(channel.type) : of$(null)),
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
@@ -22,7 +23,6 @@ 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: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
|
||||
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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: post.channel.observe().pipe(map((channel: ChannelModel) => channel.teamId)),
|
||||
teamID: observeChannel(database, post.channelId).pipe(map((channel) => channel?.teamId)),
|
||||
currentTeamId: observeCurrentTeamId(database),
|
||||
}));
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {getPreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
|
||||
import {queryDisplayNamePreferences} 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 = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY).
|
||||
const linkPreviewPreference = queryDisplayNamePreferences(database, Preferences.LINK_PREVIEW_DISPLAY).
|
||||
observeWithColumns(['value']);
|
||||
const showLinkPreviews = combineLatest([linkPreviewsConfig, linkPreviewPreference], (cfg, pref) => {
|
||||
const previewsEnabled = getPreferenceAsBool(pref, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.LINK_PREVIEW_DISPLAY, true);
|
||||
const previewsEnabled = getDisplayNamePreferenceAsBool(pref, Preferences.LINK_PREVIEW_DISPLAY, true);
|
||||
return of$(previewsEnabled && cfg);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {LayoutChangeEvent, StyleProp, Text, View, ViewStyle} from 'react-native';
|
||||
import {LayoutChangeEvent, StyleProp, 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';
|
||||
@@ -39,7 +38,6 @@ type BodyProps = {
|
||||
post: PostModel;
|
||||
searchPatterns?: SearchPattern[];
|
||||
showAddReaction?: boolean;
|
||||
voiceMessageEnabled?: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
@@ -79,7 +77,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const Body = ({
|
||||
appsEnabled, hasFiles, hasReactions, highlight, highlightReplyBar,
|
||||
isCRTEnabled, isEphemeral, isFirstReply, isJumboEmoji, isLastReply, isPendingOrFailed, isPostAddChannelMember,
|
||||
location, post, searchPatterns, showAddReaction, voiceMessageEnabled, theme,
|
||||
location, post, searchPatterns, showAddReaction, theme,
|
||||
}: BodyProps) => {
|
||||
const style = getStyleSheet(theme);
|
||||
const isEdited = postEdited(post);
|
||||
@@ -161,52 +159,36 @@ const Body = ({
|
||||
}
|
||||
|
||||
if (!hasBeenDeleted) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -42,7 +43,7 @@ const withReactions = withObservables(['post'], ({database, post}: WithReactions
|
||||
currentUserId,
|
||||
disabled,
|
||||
postId: of$(post.id),
|
||||
reactions: post.reactions.observe(),
|
||||
reactions: observeReactionsForPost(database, post.id),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const enhanced = withObservables(
|
||||
({database, thread}: WithDatabaseArgs & {thread: ThreadModel}) => {
|
||||
return {
|
||||
participants: queryThreadParticipants(database, thread.id).observe(),
|
||||
teamId: observeTeamIdByThread(thread),
|
||||
teamId: observeTeamIdByThread(database, thread),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user