forked from Ivasoft/mattermost-mobile
Compare commits
24 Commits
test1.0.5
...
release-2.
| 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 |
37
.drone.yml
37
.drone.yml
@@ -1,37 +0,0 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: permissions
|
||||
image: alpine/git
|
||||
commands:
|
||||
- chmod -R 777 .
|
||||
|
||||
- name: build
|
||||
image: cimg/android:2022.09.2-node
|
||||
environment:
|
||||
CIRCLECI: true
|
||||
NODE_OPTIONS: --max_old_space_size=12000
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
BUILD_FOR_RELEASE: true
|
||||
#APP_NAME: "EXprojekt Team Beta"
|
||||
#APP_SCHEME=exprojekt
|
||||
#REPLACE_ASSETS=true
|
||||
#MAIN_APP_IDENTIFIER=cz.exprojekt.team.beta
|
||||
#SUPPLY_PACKAGE_NAME=cz.exprojekt.team.beta
|
||||
MATTERMOST_RELEASE_STORE_FILE: /root/mattermost.keystore
|
||||
MATTERMOST_RELEASE_KEY_ALIAS: mattermost-google-key
|
||||
MATTERMOST_RELEASE_PASSWORD: 123456
|
||||
commands:
|
||||
- 'npm run build:android'
|
||||
|
||||
- name: gitea_release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key:
|
||||
from_secret: drone_release
|
||||
base_url: https://git.ivasoft.cz
|
||||
files: '*.apk'
|
||||
when:
|
||||
event: tag
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,9 +7,6 @@ mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
*/**/compass-icons.ttf
|
||||
|
||||
@@ -33,6 +30,8 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://mattermost.com/suggestions/. File non-security related bugs here in the following format:
|
||||
Submit feature requests to https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply plugin: 'kotlin-android'
|
||||
import com.android.build.OutputFile
|
||||
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
@@ -110,7 +112,7 @@ android {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 461
|
||||
versionCode 457
|
||||
versionName "2.1.0"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -145,7 +147,6 @@ android {
|
||||
release {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
|
||||
if (useReleaseKey) {
|
||||
signingConfig signingConfigs.release
|
||||
} else {
|
||||
@@ -171,10 +172,10 @@ android {
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits
|
||||
def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.filters[0]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi.identifier) * 2000000 + defaultConfig.versionCode
|
||||
versionCodes.get(abi) * 2000000 + defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +191,7 @@ dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
@@ -204,19 +205,18 @@ dependencies {
|
||||
implementation jscFlavor
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
|
||||
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.6.1'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation "com.google.firebase:firebase-messaging:$firebaseVersion"
|
||||
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
implementation project(':reactnativenotifications')
|
||||
implementation project(':watermelondb')
|
||||
implementation project(':watermelondb-jsi')
|
||||
}
|
||||
|
||||
@@ -224,16 +224,16 @@ configurations.all {
|
||||
resolutionStrategy {
|
||||
eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.name == 'play-services-base') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-tasks') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.0.2'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-stats') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '17.0.3'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'play-services-basement') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '18.1.0'
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
|
||||
}
|
||||
if (details.requested.name == 'okhttp') {
|
||||
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.10.0'
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import com.mattermost.networkclient.RCTOkHttpClientFactory;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
@@ -37,9 +37,13 @@ public class ReactNativeFlipper {
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
RCTOkHttpClientFactory.Companion.setFlipperPlugin(networkFlipperPlugin);
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
builder -> builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)));
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
@@ -52,7 +56,12 @@ public class ReactNativeFlipper {
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
() -> client.addPlugin(new FrescoFlipperPlugin()));
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.util.LruCache
|
||||
|
||||
class BitmapCache {
|
||||
private var memoryCache: LruCache<String, Bitmap>
|
||||
private var keysCache: LruCache<String, String>
|
||||
|
||||
init {
|
||||
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
|
||||
@@ -15,35 +14,15 @@ class BitmapCache {
|
||||
return bitmap.byteCount / 1024
|
||||
}
|
||||
}
|
||||
keysCache = LruCache<String, String>(50)
|
||||
}
|
||||
|
||||
fun bitmap(userId: String, updatedAt: Double, serverUrl: String): Bitmap? {
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
fun getBitmapFromMemCache(key: String): Bitmap? {
|
||||
return memoryCache.get(key)
|
||||
}
|
||||
|
||||
fun insertBitmap(bitmap: Bitmap?, userId: String, updatedAt: Double, serverUrl: String) {
|
||||
if (bitmap == null) {
|
||||
removeBitmap(userId, serverUrl)
|
||||
fun addBitmapToMemoryCache(key: String, bitmap: Bitmap) {
|
||||
if (getBitmapFromMemCache(key) == null) {
|
||||
memoryCache.put(key, bitmap)
|
||||
}
|
||||
val key = "$serverUrl-$userId-$updatedAt"
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
keysCache.put(cachedKey, key)
|
||||
memoryCache.put(key, bitmap)
|
||||
}
|
||||
|
||||
fun removeBitmap(userId: String, serverUrl: String) {
|
||||
val cachedKey = "$serverUrl-$userId"
|
||||
val key = keysCache.get(cachedKey)
|
||||
if (key != null) {
|
||||
memoryCache.remove(key)
|
||||
keysCache.remove(cachedKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllBitmaps() {
|
||||
memoryCache.evictAll()
|
||||
keysCache.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class Credentials {
|
||||
String service = map.getString("service");
|
||||
assert service != null;
|
||||
if (service.isEmpty()) {
|
||||
String[] credentials = token[0].split(", *");
|
||||
String[] credentials = token[0].split(",[ ]*");
|
||||
if (credentials.length == 2) {
|
||||
token[0] = credentials[0];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.Person;
|
||||
@@ -29,7 +28,6 @@ import androidx.core.app.RemoteInput;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import com.mattermost.rnbeta.*;
|
||||
import com.nozbe.watermelondb.Database;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
@@ -39,9 +37,6 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.getDatabaseForServer;
|
||||
import static com.mattermost.helpers.database_extension.UserKt.getLastPictureUpdate;
|
||||
|
||||
public class CustomPushNotificationHelper {
|
||||
public static final String CHANNEL_HIGH_IMPORTANCE_ID = "channel_01";
|
||||
public static final String CHANNEL_MIN_IMPORTANCE_ID = "channel_02";
|
||||
@@ -60,7 +55,7 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
private static final BitmapCache bitmapCache = new BitmapCache();
|
||||
|
||||
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
|
||||
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");
|
||||
@@ -82,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));
|
||||
}
|
||||
@@ -128,7 +123,6 @@ public class CustomPushNotificationHelper {
|
||||
notification.addExtras(userInfoBundle);
|
||||
}
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private static void addNotificationReplyAction(Context context, NotificationCompat.Builder notification, Bundle bundle, int notificationId) {
|
||||
String postId = bundle.getString("post_id");
|
||||
String serverUrl = bundle.getString("server_url");
|
||||
@@ -185,8 +179,8 @@ 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);
|
||||
|
||||
@@ -262,7 +256,7 @@ public class CustomPushNotificationHelper {
|
||||
return title;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -370,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);
|
||||
}
|
||||
|
||||
@@ -384,7 +378,7 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
|
||||
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");
|
||||
@@ -395,7 +389,7 @@ public class CustomPushNotificationHelper {
|
||||
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);
|
||||
}
|
||||
@@ -405,33 +399,19 @@ public class CustomPushNotificationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap userAvatar(final Context context, @NonNull 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 {
|
||||
Response response;
|
||||
Double lastUpdateAt = 0.0;
|
||||
if (!TextUtils.isEmpty(urlOverride)) {
|
||||
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 {
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
if (dbHelper != null) {
|
||||
Database db = getDatabaseForServer(dbHelper, context, serverUrl);
|
||||
if (db != null) {
|
||||
lastUpdateAt = getLastPictureUpdate(db, userId);
|
||||
if (lastUpdateAt == null) {
|
||||
lastUpdateAt = 0.0;
|
||||
}
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
Bitmap cached = bitmapCache.bitmap(userId, lastUpdateAt, serverUrl);
|
||||
Bitmap cached = bitmapCache.getBitmapFromMemCache(userId);
|
||||
if (cached != null) {
|
||||
Bitmap bitmap = cached.copy(cached.getConfig(), false);
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
bitmapCache.removeBitmap(userId, serverUrl);
|
||||
String url = String.format("api/v4/users/%s/image", userId);
|
||||
Log.i("ReactNative", String.format("Fetch profile image %s", url));
|
||||
response = Network.getSync(serverUrl, url, null);
|
||||
@@ -442,7 +422,7 @@ public class CustomPushNotificationHelper {
|
||||
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
||||
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
|
||||
bitmapCache.insertBitmap(bitmap.copy(bitmap.getConfig(), false), userId, lastUpdateAt, serverUrl);
|
||||
bitmapCache.addBitmapToMemoryCache(userId, bitmap.copy(bitmap.getConfig(), false));
|
||||
}
|
||||
return getCircleBitmap(bitmap);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,32 @@ package com.mattermost.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.lang.Exception
|
||||
import java.util.*
|
||||
|
||||
class DatabaseHelper {
|
||||
var defaultDatabase: Database? = null
|
||||
private var defaultDatabase: Database? = null
|
||||
|
||||
val onlyServerUrl: String?
|
||||
get() {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
|
||||
defaultDatabase!!.rawQuery(query).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
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()
|
||||
@@ -36,13 +42,640 @@ class DatabaseHelper {
|
||||
}
|
||||
}
|
||||
|
||||
fun getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(identifier)
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val url = cursor.getString(0)
|
||||
cursor.close()
|
||||
return url
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
val args: Array<Any?> = arrayOf(id)
|
||||
try {
|
||||
db.rawQuery("select * from $tableName where id == ? limit 1", args).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val args: Array<Any?> = arrayOf(serverUrl)
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
val cursor = defaultDatabase!!.rawQuery(query, args)
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
cursor.close()
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")!!
|
||||
return result.getString("value")
|
||||
}
|
||||
|
||||
private fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
cursor1.close()
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
val cursor2 = db.rawQuery(postQuery, arrayOf(channelId, earliest, latest))
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
val createAt = cursor2.getDouble(0)
|
||||
cursor2.close()
|
||||
return createAt
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val lastFetchedAt = cursor1.getDouble(0)
|
||||
cursor1.close()
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
var lastFetchedAt = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
val post = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val rootId = post["root_id"] as? String
|
||||
|
||||
if (!rootId.isNullOrEmpty()) {
|
||||
var thread = postsInThread[rootId]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[rootId] = thread.toList()
|
||||
}
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
updateMyChannelLastFetchedAt(db, channelId, lastFetchedAt)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ReadableArray) {
|
||||
for (i in 0 until threads.size()) {
|
||||
val thread = threads.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
|
||||
// Insert/Update the thread
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
// Delete existing and insert thread participants
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("delete from ThreadParticipant where thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
|
||||
db.execute(
|
||||
"insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " +
|
||||
"last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, " +
|
||||
"props, timezone, _status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"),
|
||||
user.getDouble("update_at"),
|
||||
user.getDouble("delete_at"),
|
||||
user.getString("email"),
|
||||
user.getString("first_name"),
|
||||
isBot,
|
||||
roles.contains("system_guest"),
|
||||
user.getString("last_name"),
|
||||
lastPictureUpdate,
|
||||
user.getString("locale"),
|
||||
user.getString("nickname"),
|
||||
user.getString("position"),
|
||||
roles,
|
||||
"",
|
||||
user.getString("username"),
|
||||
"{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props") ?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone") ?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultDatabase(context: Context) {
|
||||
val databaseName = "app.db"
|
||||
val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName
|
||||
defaultDatabase = Database(databasePath, context)
|
||||
}
|
||||
|
||||
internal fun JSONObject.toMap(): Map<String, Any?> = keys().asSequence().associateWith { it ->
|
||||
private fun insertPost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
var files: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
files = metadata.remove("files") as JSONArray?
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"insert into Post " +
|
||||
"(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, " +
|
||||
"previous_post_id, root_id, type, user_id, props, _status)" +
|
||||
" values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
post.getString("id"),
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString()
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePost(db: Database, post: JSONObject) {
|
||||
var metadata: JSONObject?
|
||||
var reactions: JSONArray? = null
|
||||
var customEmojis: JSONArray? = null
|
||||
|
||||
try {
|
||||
metadata = post.getJSONObject("metadata")
|
||||
reactions = metadata.remove("reactions") as JSONArray?
|
||||
customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
metadata.remove("files")
|
||||
} catch (e: Exception) {
|
||||
// no metadata found
|
||||
metadata = JSONObject()
|
||||
}
|
||||
|
||||
db.execute(
|
||||
"update Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?, " +
|
||||
"is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?, " +
|
||||
"root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated' " +
|
||||
"where id = ?",
|
||||
arrayOf(
|
||||
post.getString("channel_id"),
|
||||
post.getDouble("create_at"),
|
||||
post.getDouble("delete_at"),
|
||||
post.getDouble("update_at"),
|
||||
post.getDouble("edit_at"),
|
||||
post.getBoolean("is_pinned"),
|
||||
post.getString("message"),
|
||||
metadata.toString(),
|
||||
post.getString("original_id"),
|
||||
post.getString("pending_post_id"),
|
||||
post.getString("prev_post_id"),
|
||||
post.getString("root_id"),
|
||||
post.getString("type"),
|
||||
post.getString("user_id"),
|
||||
post.getJSONObject("props").toString(),
|
||||
post.getString("id"),
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("delete from Reaction where post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"insert into Thread " +
|
||||
"(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, _status)" +
|
||||
" values (?, ?, 0, ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
thread.getString("id"),
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"update Thread SET last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?, unread_mentions = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
lastReplyAt,
|
||||
lastViewedAt,
|
||||
replyCount,
|
||||
isFollowing,
|
||||
unreadReplies,
|
||||
unreadMentions,
|
||||
thread.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into ThreadParticipant " +
|
||||
"(id, thread_id, user_id, _status)" +
|
||||
" values (?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
threadId,
|
||||
participant.getString("id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if(find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"insert into CustomEmoji (id, name, _status) values (?, ?, 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertFiles(db: Database, files: JSONArray) {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " +
|
||||
"values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
file.getString("id"),
|
||||
file.getString("extension"),
|
||||
height,
|
||||
miniPreview,
|
||||
file.getString("mime_type"),
|
||||
file.getString("name"),
|
||||
file.getString("post_id"),
|
||||
file.getDouble("size"),
|
||||
width
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into Reaction (id, create_at, emoji_name, post_id, user_id, _status) " +
|
||||
"values (?, ?, ?, ?, ?, 'created')",
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"),
|
||||
reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"),
|
||||
reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
db.rawQuery("select id, channel_id, earliest, latest from PostsInChannel where channel_id = ?", arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"update PostsInChannel set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
mergePostsInChannel(db, resultArray, newChunk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyChannelLastFetchedAt(db: Database, channelId: String, lastFetchedAt: Double) {
|
||||
db.execute(
|
||||
"UPDATE MyChannel SET last_fetched_at = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
lastFetchedAt,
|
||||
channelId
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap {
|
||||
val id = RandomId.generate()
|
||||
db.execute("insert into PostsInChannel (id, channel_id, earliest, latest, _status) values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
return map
|
||||
}
|
||||
|
||||
private fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("delete from PostsInChannel where id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("select * from PostsInThread where root_id = ? order by latest desc", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
db.execute(
|
||||
"update PostsInThread set earliest = ?, latest = ?, _status = 'updated' where id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, cursorMap.getDouble("earliest")),
|
||||
maxOf(latest, cursorMap.getDouble("latest")),
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"insert into PostsInThread (id, root_id, earliest, latest, _status) " +
|
||||
"values (?, ?, ?, ?, 'created')",
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toMap(): Map<String, *> = keys().asSequence().associateWith { it ->
|
||||
when (val value = this[it])
|
||||
{
|
||||
is JSONArray ->
|
||||
@@ -50,15 +683,9 @@ class DatabaseHelper {
|
||||
val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) }
|
||||
JSONObject(map).toMap().values.toList()
|
||||
}
|
||||
is JSONObject -> {
|
||||
value.toMap()
|
||||
}
|
||||
JSONObject.NULL -> {
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
value
|
||||
}
|
||||
is JSONObject -> value.toMap()
|
||||
JSONObject.NULL -> null
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,151 +4,292 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.mattermost.helpers.push_notification.*
|
||||
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class PushNotificationDataHelper(private val context: Context) {
|
||||
private var coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
var result: Bundle? = null
|
||||
val job = coroutineScope.launch(Dispatchers.Default) {
|
||||
result = PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
runBlocking {
|
||||
job.join()
|
||||
}
|
||||
|
||||
return result
|
||||
private var scope = Executors.newSingleThreadExecutor()
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle) {
|
||||
scope.execute(Runnable {
|
||||
runBlocking {
|
||||
PushNotificationDataRunnable.start(context, initialData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
internal val specialMentions = listOf("all", "here", "channel")
|
||||
private val dbHelper = DatabaseHelper.instance!!
|
||||
private val mutex = Mutex()
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||
mutex.withLock {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return null
|
||||
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||
var result: Bundle? = null
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
try {
|
||||
if (db != null) {
|
||||
val teamId = initialData.getString("team_id")
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val postId = initialData.getString("post_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
var posts: ReadableMap? = null
|
||||
var userIdsToLoad: ReadableArray? = null
|
||||
var usernamesToLoad: ReadableArray? = null
|
||||
|
||||
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId")
|
||||
var threads: ReadableArray? = null
|
||||
var usersFromThreads: ReadableArray? = null
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val notificationData = Arguments.createMap()
|
||||
coroutineScope {
|
||||
if (channelId != null) {
|
||||
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
|
||||
|
||||
if (!teamId.isNullOrEmpty()) {
|
||||
val res = fetchTeamIfNeeded(db, serverUrl, teamId)
|
||||
res.first?.let { notificationData.putMap("team", it) }
|
||||
res.second?.let { notificationData.putMap("myTeam", it) }
|
||||
}
|
||||
posts = postData?.getMap("posts")
|
||||
userIdsToLoad = postData?.getArray("userIdsToLoad")
|
||||
usernamesToLoad = postData?.getArray("usernamesToLoad")
|
||||
threads = postData?.getArray("threads")
|
||||
usersFromThreads = postData?.getArray("usersFromThreads")
|
||||
|
||||
if (channelId != null && postId != null) {
|
||||
val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled)
|
||||
channelRes.first?.let { notificationData.putMap("channel", it) }
|
||||
channelRes.second?.let { notificationData.putMap("myChannel", it) }
|
||||
val loadedProfiles = channelRes.third
|
||||
|
||||
// Fetch categories if needed
|
||||
if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) {
|
||||
// should load all categories
|
||||
val res = fetchMyTeamCategories(db, serverUrl, teamId)
|
||||
res?.let { notificationData.putMap("categories", it) }
|
||||
} else if (notificationData.getMap("channel") != null) {
|
||||
// check if the channel is in the category for the team
|
||||
val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!)
|
||||
res?.let { notificationData.putArray("categoryChannels", it) }
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
|
||||
userIdsToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
val postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles)
|
||||
postData?.getMap("posts")?.let { notificationData.putMap("posts", it) }
|
||||
|
||||
var notificationThread: ReadableMap? = null
|
||||
if (isCRTEnabled && !rootId.isNullOrEmpty()) {
|
||||
notificationThread = fetchThread(db, serverUrl, rootId, teamId)
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!)
|
||||
usernamesToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for(item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
}
|
||||
|
||||
val userList = fetchNeededUsers(serverUrl, loadedProfiles, postData)
|
||||
notificationData.putArray("users", ReadableArrayUtils.toWritableArray(userList.toArray()))
|
||||
}
|
||||
|
||||
result = Arguments.toBundle(notificationData)
|
||||
|
||||
if (!isReactInit) {
|
||||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
db?.close()
|
||||
Log.i("ReactNative", "DONE fetching notification data")
|
||||
}
|
||||
|
||||
return result
|
||||
db.transaction {
|
||||
if (posts != null && channelId != null) {
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
DatabaseHelper.instance!!.handleThreads(db, threads!!)
|
||||
}
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, userIdsToLoad!!)
|
||||
}
|
||||
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
|
||||
}
|
||||
|
||||
if (usersFromThreads != null) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadList(notificationThread: ReadableMap?, threads: ReadableArray?): ArrayList<ReadableMap>? {
|
||||
threads?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
val threadIds = ArrayList<String>()
|
||||
notificationThread?.let { thread ->
|
||||
thread.getString("id")?.let { it1 -> threadIds.add(it1) }
|
||||
threadsArray.add(thread)
|
||||
}
|
||||
for(i in 0 until it.size()) {
|
||||
val thread = it.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
if (threadId != null) {
|
||||
if (threadIds.contains(threadId)) {
|
||||
// replace the values for participants and is_following
|
||||
val index = threadsArray.indexOfFirst { el -> el.getString("id") == threadId }
|
||||
val prev = threadsArray[index]
|
||||
val merge = Arguments.createMap()
|
||||
merge.merge(prev)
|
||||
merge.putBoolean("is_following", thread.getBoolean("is_following"))
|
||||
merge.putArray("participants", thread.getArray("participants"))
|
||||
threadsArray[index] = merge
|
||||
} else {
|
||||
threadsArray.add(thread)
|
||||
threadIds.add(threadId)
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
|
||||
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
|
||||
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
|
||||
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
|
||||
while(iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post.getArray("participants")
|
||||
if (participants != null) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = DatabaseHelper.instance!!.queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = DatabaseHelper.instance!!.queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach{ (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
return threadsArray
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
return null
|
||||
private suspend fun fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
|
||||
@@ -110,9 +109,7 @@ public class ReadableArrayUtils {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value instanceof ReadableMap) {
|
||||
writableArray.pushMap((ReadableMap) value);
|
||||
}else if (value.getClass().isArray()) {
|
||||
} else if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,37 +122,24 @@ public class RealPathUtil {
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
|
||||
boolean cacheDirExists = cacheDir.exists();
|
||||
if (!cacheDirExists) {
|
||||
cacheDirExists = cacheDir.mkdirs();
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
if (cacheDirExists) {
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
boolean fileCreated = tmpFile.createNewFile();
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
if (fileCreated) {
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
try (FileInputStream inputSrc = new FileInputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel src = inputSrc.getChannel();
|
||||
try (FileOutputStream outputDst = new FileOutputStream(tmpFile)) {
|
||||
FileChannel dst = outputDst.getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
}
|
||||
}
|
||||
|
||||
pfd.close();
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
@@ -258,9 +245,7 @@ public class RealPathUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileOrDirectory.delete()) {
|
||||
Log.i("ReactNative", "Couldn't delete file " + fileOrDirectory.getName());
|
||||
}
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String filename) {
|
||||
@@ -271,4 +256,22 @@ public class RealPathUtil {
|
||||
File f = new File(filename);
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
public static File createDirIfNotExists(String path) {
|
||||
File dir = new File(path);
|
||||
if (dir.exists()) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
try {
|
||||
dir.mkdirs();
|
||||
// Add .nomedia to hide the thumbnail directory from gallery
|
||||
File noMedia = new File(path, ".nomedia");
|
||||
noMedia.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun insertCategory(db: Database, category: ReadableMap) {
|
||||
try {
|
||||
val id = category.getString("id") ?: return
|
||||
val collapsed = false
|
||||
val displayName = category.getString("display_name")
|
||||
val muted = category.getBoolean("muted")
|
||||
val sortOrder = category.getInt("sort_order")
|
||||
val sorting = category.getString("sorting") ?: "recent"
|
||||
val teamId = category.getString("team_id")
|
||||
val type = category.getString("type")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Category
|
||||
(id, collapsed, display_name, muted, sort_order, sorting, team_id, type, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, collapsed, displayName, muted,
|
||||
sortOrder / 10, sorting, teamId, type
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until channelIds.size()) {
|
||||
val channelId = channelIds.getString(i)
|
||||
val id = "${teamId}_$channelId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, i)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
|
||||
val categories = orderCategories.getArray("categories") ?: return
|
||||
for (i in 0 until categories.size()) {
|
||||
val category = categories.getMap(i)
|
||||
val id = category.getString("id")
|
||||
val teamId = category.getString("team_id")
|
||||
val channelIds = category.getArray("channel_ids")
|
||||
insertCategory(db, category)
|
||||
if (id != null && teamId != null) {
|
||||
channelIds?.let { insertCategoryChannels(db, id, teamId, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until categoryChannels.size()) {
|
||||
val cc = categoryChannels.getMap(i)
|
||||
val id = cc.getString("id")
|
||||
val categoryId = cc.getString("category_id")
|
||||
val channelId = cc.getString("channel_id")
|
||||
val count = countByColumn(db, "CategoryChannel", "category_id", categoryId)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, if (count > 0) count + 1 else count)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun findChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Channel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyChannel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun handleChannel(db: Database, channel: ReadableMap) {
|
||||
try {
|
||||
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
|
||||
if (!exists) {
|
||||
val json = ReadableMapUtils.toJSONObject(channel)
|
||||
if (insertChannel(db, json)) {
|
||||
insertChannelInfo(db, json)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
try {
|
||||
val json = ReadableMapUtils.toJSONObject(myChannel)
|
||||
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
|
||||
|
||||
if (postsData != null && !receivingThreads) {
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val postList = posts.toList()
|
||||
val lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
json.put("last_fetched_at", lastFetchedAt)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
updateMyChannel(db, json)
|
||||
return
|
||||
}
|
||||
|
||||
if (insertMyChannel(db, json)) {
|
||||
insertMyChannelSettings(db, json)
|
||||
insertChannelMember(db, json)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannel(db: Database, channel: JSONObject): Boolean {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return false }
|
||||
val createAt = try { channel.getDouble("create_at") } catch (e: JSONException) { 0 }
|
||||
val deleteAt = try { channel.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { channel.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val creatorId = try { channel.getString("creator_id") } catch (e: JSONException) { "" }
|
||||
val displayName = try { channel.getString("display_name") } catch (e: JSONException) { "" }
|
||||
val name = try { channel.getString("name") } catch (e: JSONException) { "" }
|
||||
val teamId = try { channel.getString("team_id") } catch (e: JSONException) { "" }
|
||||
val type = try { channel.getString("type") } catch (e: JSONException) { "O" }
|
||||
val isGroupConstrained = try { channel.getBoolean("group_constrained") } catch (e: JSONException) { false }
|
||||
val shared = try { channel.getBoolean("shared") } catch (e: JSONException) { false }
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Channel
|
||||
(id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, createAt, deleteAt, updateAt,
|
||||
creatorId, displayName, name, teamId, type,
|
||||
isGroupConstrained, shared
|
||||
)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelInfo(db: Database, channel: JSONObject) {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return }
|
||||
val header = try { channel.getString("header") } catch (e: JSONException) { "" }
|
||||
val purpose = try { channel.getString("purpose") } catch (e: JSONException) { "" }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelInfo
|
||||
(id, header, purpose, guest_count, member_count, pinned_post_count, _changed, _status)
|
||||
VALUES (?, ?, ?, 0, 0, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, header, purpose)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean {
|
||||
return try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return false }
|
||||
val roles = try { myChanel.getString("roles") } catch (e: JSONException) { "" }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val viewedAt = 0
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
val manuallyUnread = false
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannel
|
||||
(id, roles, message_count, mentions_count, is_unread, manually_unread,
|
||||
last_post_at, last_viewed_at, viewed_at, last_fetched_at, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(
|
||||
id, roles, msgCount, mentionsCount, isUnread, manuallyUnread,
|
||||
lastPostAt, lastViewedAt, viewedAt, lastFetchedAt
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannelSettings (id, notify_props, _changed, _status)
|
||||
VALUES (?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, notifyProps)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelMember(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val userId = queryCurrentUserId(db) ?: return
|
||||
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val schemeAdmin = try { myChanel.getBoolean("scheme_admin") } catch (e: JSONException) { false }
|
||||
val id = "$channelId-$userId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelMembership
|
||||
(id, channel_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, channelId, userId, schemeAdmin)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMyChannel(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE MyChannel SET message_count=?, mentions_count=?, is_unread=?,
|
||||
last_post_at=?, last_viewed_at=?, last_fetched_at=?, _status = 'updated'
|
||||
WHERE id=?
|
||||
""",
|
||||
arrayOf(
|
||||
msgCount, mentionsCount, isUnread,
|
||||
lastPostAt, lastViewedAt, lastFetchedAt, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
try {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if (find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"INSERT INTO CustomEmoji (id, name, _changed, _status) VALUES (?, ?, '', 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
internal fun insertFiles(db: Database, files: JSONArray) {
|
||||
try {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val id = file.getString("id")
|
||||
val extension = file.getString("extension")
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val mime = file.getString("mime_type")
|
||||
val name = file.getString("name")
|
||||
val postId = file.getString("post_id")
|
||||
val size = try { file.getDouble("size") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO File
|
||||
(id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, extension, height, miniPreview,
|
||||
mime, name, postId, size, width
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.QueryArgs
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import java.util.*
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
val posts = data.getMap("posts")
|
||||
data.getMap("team")?.let { insertTeam(db, it) }
|
||||
data.getMap("myTeam")?.let { insertMyTeam(db, it) }
|
||||
data.getMap("channel")?.let { handleChannel(db, it) }
|
||||
data.getMap("myChannel")?.let { handleMyChannel(db, it, posts, receivingThreads) }
|
||||
data.getMap("categories")?.let { insertCategoriesWithChannels(db, it) }
|
||||
data.getArray("categoryChannels")?.let { insertChannelToDefaultCategory(db, it) }
|
||||
if (channelId != null) {
|
||||
handlePosts(db, posts, channelId, receivingThreads)
|
||||
}
|
||||
data.getArray("threads")?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
for (i in 0 until it.size()) {
|
||||
threadsArray.add(it.getMap(i))
|
||||
}
|
||||
handleThreads(db, threadsArray, teamId)
|
||||
}
|
||||
data.getArray("users")?.let { handleUsers(db, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(identifier)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
|
||||
arrayOf(id)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
try {
|
||||
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE $whereString LIMIT 1",
|
||||
values
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
db.rawQuery("SELECT DISTINCT id FROM $tableName WHERE id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("SELECT DISTINCT $columnName FROM $tableName WHERE $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",
|
||||
arrayOf(value)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return 0
|
||||
}
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor1 ->
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
|
||||
db.rawQuery(postQuery, arrayOf(channelId, earliest, latest)).use { cursor2 ->
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
return cursor2.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val lastFetchedAt = cursor.getDouble(0)
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryLastPostInThread(db: Database?, rootId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1"
|
||||
db.rawQuery(query, arrayOf(rootId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
val files = metadata.remove("files") as JSONArray?
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Post
|
||||
(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id,
|
||||
previous_post_id, root_id, type, user_id, props, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updatePost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
|
||||
metadata.remove("files")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?,
|
||||
is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?,
|
||||
root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated'
|
||||
WHERE id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props,
|
||||
id,
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("DELETE FROM Reaction WHERE post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
try {
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
@Suppress("UNCHECKED_CAST", "UNCHECKED_CAST")
|
||||
val post: MutableMap<String, Any?> = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val postId = post["id"] as? String ?: ""
|
||||
val rootId = post["root_id"] as? String ?: ""
|
||||
val postInThread = rootId.ifEmpty { postId }
|
||||
var thread = postsInThread[postInThread]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[postInThread] = thread.toList()
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
|
||||
return try {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInChannel
|
||||
(id, channel_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
try {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("DELETE FROM PostsInChannel WHERE id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",
|
||||
arrayOf(channelId)
|
||||
).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"UPDATE PostsInChannel SET earliest = ?, latest = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
newChunk?.let { mergePostsInChannel(db, resultArray, it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun getTeammateDisplayNameSetting(db: Database): String {
|
||||
val configSetting = queryConfigDisplayNameSetting(db)
|
||||
if (configSetting != null) {
|
||||
return configSetting
|
||||
}
|
||||
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT value FROM Preference where category = ? AND name = ? limit 1",
|
||||
arrayOf("display_settings", "name_format")
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return "username"
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap?.getString("value") ?: "username"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return "username"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
try {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Reaction
|
||||
(id, create_at, emoji_name, post_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"), reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"), reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONObject
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryCurrentTeamId(db: Database): String? {
|
||||
val result = find(db, "System", "currentTeamId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryConfigDisplayNameSetting(db: Database): String? {
|
||||
val license = find(db, "System", "license")
|
||||
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
|
||||
val displayName = find(db, "Config", "TeammateNameDisplay")
|
||||
|
||||
val licenseValue = license?.getString("value") ?: ""
|
||||
val lockDisplayNameValue = lockDisplayName?.getString("value") ?: "false"
|
||||
val displayNameValue = displayName?.getString("value") ?: "full_name"
|
||||
val licenseJson = JSONObject(licenseValue)
|
||||
val licenseLock = try { licenseJson.getString("LockTeammateNameDisplay") } catch (e: Exception) { "false"}
|
||||
|
||||
if (licenseLock == "true" && lockDisplayNameValue == "true") {
|
||||
return displayNameValue
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun findTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Team", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyTeam", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
|
||||
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
|
||||
val results = ArrayList<ReadableMap>()
|
||||
if (cursor.count > 0) {
|
||||
while(cursor.moveToNext()) {
|
||||
val map = Arguments.createMap()
|
||||
map.mapCursor(cursor)
|
||||
results.add(map)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun insertTeam(db: Database, team: ReadableMap): Boolean {
|
||||
val id = try { team.getString("id") } catch (e: Exception) { return false }
|
||||
val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 }
|
||||
if (deleteAt.toInt() > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isAllowOpenInvite = try { team.getBoolean("allow_open_invite") } catch (e: NoSuchKeyException) { false }
|
||||
val description = try { team.getString("description") } catch (e: NoSuchKeyException) { "" }
|
||||
val displayName = try { team.getString("display_name") } catch (e: NoSuchKeyException) { "" }
|
||||
val name = try { team.getString("name") } catch (e: NoSuchKeyException) { "" }
|
||||
val updateAt = try { team.getDouble("update_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val type = try { team.getString("type") } catch (e: NoSuchKeyException) { "O" }
|
||||
val allowedDomains = try { team.getString("allowed_domains") } catch (e: NoSuchKeyException) { "" }
|
||||
val isGroupConstrained = try { team.getBoolean("group_constrained") } catch (e: NoSuchKeyException) { false }
|
||||
val lastTeamIconUpdatedAt = try { team.getDouble("last_team_icon_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
val inviteId = try { team.getString("invite_id") } catch (e: NoSuchKeyException) { "" }
|
||||
val status = "created"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Team (
|
||||
id, allow_open_invite, description, display_name, name, update_at, type, allowed_domains,
|
||||
group_constrained, last_team_icon_update, invite_id, _changed, _status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, isAllowOpenInvite, description, displayName, name, updateAt,
|
||||
type, allowedDomains, isGroupConstrained, lastTeamIconUpdatedAt, inviteId, status
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyTeam(db: Database, myTeam: ReadableMap): Boolean {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return false
|
||||
val id = try { myTeam.getString("id") } catch (e: NoSuchKeyException) { return false }
|
||||
val roles = try { myTeam.getString("roles") } catch (e: NoSuchKeyException) { "" }
|
||||
val schemeAdmin = try { myTeam.getBoolean("scheme_admin") } catch (e: NoSuchKeyException) { false }
|
||||
val status = "created"
|
||||
val membershipId = "$id-$currentUserId"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"INSERT INTO MyTeam (id, roles, _changed, _status) VALUES (?, ?, '', ?)",
|
||||
arrayOf(id, roles, status)
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(membershipId, id, currentUserId, schemeAdmin, status)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONObject
|
||||
|
||||
internal fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
try {
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Thread
|
||||
(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, viewed_at, _changed, _status)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, lastReplyAt, lastViewedAt,
|
||||
replyCount, isFollowing, unreadReplies, unreadMentions
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
try {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Thread SET
|
||||
last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?,
|
||||
unread_mentions = ?, _status = 'updated' where id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
lastReplyAt, lastViewedAt, replyCount,
|
||||
isFollowing, unreadReplies, unreadMentions, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
try {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ThreadParticipant
|
||||
(id, thread_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, threadId, participant.getString("id"))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
val query = """
|
||||
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
|
||||
VALUES (?, '', 'created', ?, ?)
|
||||
"""
|
||||
db.execute(query, arrayOf(teamId, earliest, latest))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
try {
|
||||
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
|
||||
val query = "UPDATE TeamThreadsSync SET earliest=?, latest=? WHERE id=?"
|
||||
db.execute(query, arrayOf(storeEarliest, storeLatest, teamId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun syncParticipants(db: Database, thread: ReadableMap) {
|
||||
try {
|
||||
val threadId = thread.getString("id")
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
try {
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("SELECT * FROM PostsInThread WHERE root_id = ? ORDER BY latest DESC", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
val storeEarliest = minOf(earliest, cursorMap.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, cursorMap.getDouble("latest"))
|
||||
db.execute(
|
||||
"UPDATE PostsInThread SET earliest = ?, latest = ?, _status = 'updated' WHERE root_id = ?",
|
||||
arrayOf(
|
||||
storeEarliest,
|
||||
storeLatest,
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInThread
|
||||
(id, root_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
val teamIds = ArrayList<String>()
|
||||
if (teamId.isNullOrEmpty()) {
|
||||
val myTeams = queryMyTeams(db)
|
||||
if (myTeams != null) {
|
||||
for (myTeam in myTeams) {
|
||||
myTeam.getString("id")?.let { teamIds.add(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
teamIds.add(teamId)
|
||||
}
|
||||
|
||||
for (i in 0 until threads.size) {
|
||||
try {
|
||||
val thread = threads[i]
|
||||
handleThread(db, thread, teamIds)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamThreadsSync(db, threads, teamIds)
|
||||
}
|
||||
|
||||
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
// Insert/Update the thread
|
||||
val threadId = thread.getString("id")
|
||||
val isFollowing = thread.getBoolean("is_following")
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
syncParticipants(db, thread)
|
||||
|
||||
// this is per team
|
||||
if (isFollowing) {
|
||||
for (teamId in teamIds) {
|
||||
handleThreadInTeam(db, thread, teamId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
|
||||
val threadId = thread.getString("id") ?: return
|
||||
val existingRecord = findByColumns(
|
||||
db,
|
||||
"ThreadsInTeam",
|
||||
arrayOf("thread_id", "team_id"),
|
||||
arrayOf(threadId, teamId)
|
||||
)
|
||||
if (existingRecord == null) {
|
||||
try {
|
||||
val id = RandomId.generate()
|
||||
val query = """
|
||||
INSERT INTO ThreadsInTeam (id, team_id, thread_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
"""
|
||||
db.execute(query, arrayOf(id, teamId, threadId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTeamThreadsSync(db: Database, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
|
||||
val sortedList = threadList.filter{ it.getBoolean("is_following") }
|
||||
.sortedBy { it.getDouble("last_reply_at") }
|
||||
.map { it.getDouble("last_reply_at") }
|
||||
val earliest = sortedList.first()
|
||||
val latest = sortedList.last()
|
||||
|
||||
for (teamId in teamIds) {
|
||||
val existingTeamThreadsSync = find(db, "TeamThreadsSync", teamId)
|
||||
if (existingTeamThreadsSync == null) {
|
||||
insertTeamThreadsSync(db, teamId, earliest, latest)
|
||||
} else {
|
||||
updateTeamThreadsSync(db, teamId, earliest, latest, existingTeamThreadsSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
var id = userId
|
||||
if (userId == "me") {
|
||||
(queryCurrentUserId(db) ?: userId).also { id = it }
|
||||
}
|
||||
val userQuery = "SELECT last_picture_update FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(id)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentUserLocale(db: Database): String {
|
||||
try {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return "en"
|
||||
val userQuery = "SELECT locale FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(currentUserId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest,
|
||||
last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props,
|
||||
props, timezone, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"), user.getDouble("update_at"), user.getDouble("delete_at"),
|
||||
user.getString("email"), user.getString("first_name"), isBot,
|
||||
roles.contains("system_guest"), user.getString("last_name"), lastPictureUpdate,
|
||||
user.getString("locale"), user.getString("nickname"), user.getString("position"),
|
||||
roles, "", user.getString("username"), "{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props")
|
||||
?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone")
|
||||
?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findByColumns
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.mattermost.helpers.database_extension.queryMyTeams
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? {
|
||||
return try {
|
||||
val userId = queryCurrentUserId(db)
|
||||
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
|
||||
categories?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? {
|
||||
val channelId = channel.getString("id") ?: return null
|
||||
val channelType = channel.getString("type")
|
||||
val categoryChannels = Arguments.createArray()
|
||||
if (channelType == "D" || channelType == "G") {
|
||||
val myTeams = queryMyTeams(db)
|
||||
myTeams?.let {
|
||||
for (myTeam in it) {
|
||||
val map = categoryChannelForTeam(db, channelId, myTeam.getString("id"), "direct_messages")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val map = categoryChannelForTeam(db, channelId, channel.getString("team_id"), "channels")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
return categoryChannels
|
||||
}
|
||||
|
||||
private fun categoryChannelForTeam(db: Database, channelId: String, teamId: String?, type: String): ReadableMap? {
|
||||
teamId?.let { id ->
|
||||
val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id))
|
||||
val categoryId = category?.getString("id")
|
||||
categoryId?.let { cId ->
|
||||
val cc = findByColumns(
|
||||
db,
|
||||
"CategoryChannel",
|
||||
arrayOf("category_id", "channel_id"),
|
||||
arrayOf(cId, channelId)
|
||||
)
|
||||
if (cc == null) {
|
||||
val map = Arguments.createMap()
|
||||
map.putString("channel_id", channelId)
|
||||
map.putString("category_id", cId)
|
||||
map.putString("id", "${id}_$channelId")
|
||||
return map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findChannel
|
||||
import com.mattermost.helpers.database_extension.getCurrentUserLocale
|
||||
import com.mattermost.helpers.database_extension.getTeammateDisplayNameSetting
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
|
||||
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
|
||||
var channelData = channel?.getMap("data")
|
||||
val myChannelData = channelData?.let { fetchMyChannelData(serverUrl, channelId, isCRTEnabled, it) }
|
||||
val channelType = channelData?.getString("type")
|
||||
var profilesArray: ReadableArray? = null
|
||||
|
||||
if (channelData != null && channelType != null && !findChannel(db, channelId)) {
|
||||
val displayNameSetting = getTeammateDisplayNameSetting(db)
|
||||
|
||||
when (channelType) {
|
||||
"D" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val displayName = displayUsername(profilesArray!!.getMap(0), displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
"G" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val localeString = getCurrentUserLocale(db)
|
||||
val localeArray = localeString.split("-")
|
||||
val locale = if (localeArray.size == 1) {
|
||||
Locale(localeString)
|
||||
} else {
|
||||
Locale(localeArray[0], localeArray[1])
|
||||
}
|
||||
val displayName = displayGroupMessageName(profilesArray!!, locale, displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(channelData, myChannelData, profilesArray)
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(serverUrl: String, channelId: String, isCRTEnabled: Boolean, channelData: ReadableMap): ReadableMap? {
|
||||
try {
|
||||
val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me")
|
||||
val myChannelData = myChannel?.getMap("data")
|
||||
if (myChannelData != null) {
|
||||
val data = Arguments.createMap()
|
||||
data.merge(myChannelData)
|
||||
data.putString("id", channelId)
|
||||
|
||||
val totalMsg = if (isCRTEnabled) {
|
||||
channelData.getInt("total_msg_count_root")
|
||||
} else {
|
||||
channelData.getInt("total_msg_count")
|
||||
}
|
||||
|
||||
val myMsgCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("msg_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("msg_count")
|
||||
}
|
||||
|
||||
val mentionCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("mention_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("mention_count")
|
||||
}
|
||||
|
||||
val lastPostAt = if (isCRTEnabled) {
|
||||
try {
|
||||
channelData.getDouble("last_root_post_at")
|
||||
} catch (e: Exception) {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
} else {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
|
||||
val messageCount = 0.coerceAtLeast(totalMsg - myMsgCount)
|
||||
data.putInt("message_count", messageCount)
|
||||
data.putInt("mentions_count", mentionCount)
|
||||
data.putBoolean("is_unread", messageCount > 0)
|
||||
data.putDouble("last_post_at", lastPostAt)
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: Database, serverUrl: String, channelId: String): ReadableArray? {
|
||||
return try {
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=")
|
||||
val profilesArray = profilesInChannel?.getArray("data")
|
||||
val result = Arguments.createArray()
|
||||
if (profilesArray != null) {
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
if (profile.getString("id") != currentUserId) {
|
||||
result.pushMap(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayUsername(user: ReadableMap, displayNameSetting: String): String {
|
||||
val name = user.getString("username") ?: ""
|
||||
val nickname = user.getString("nickname")
|
||||
val firstName = user.getString("first_name") ?: ""
|
||||
val lastName = user.getString("last_name") ?: ""
|
||||
return when (displayNameSetting) {
|
||||
"nickname_full_name" -> {
|
||||
(nickname ?: "$firstName $lastName").trim()
|
||||
}
|
||||
"full_name" -> {
|
||||
"$firstName $lastName".trim()
|
||||
}
|
||||
else -> {
|
||||
name.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayGroupMessageName(profilesArray: ReadableArray, locale: Locale, displayNameSetting: String): String {
|
||||
val names = ArrayList<String>()
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
names.add(displayUsername(profile, displayNameSetting))
|
||||
}
|
||||
|
||||
return names.sortedWith { s1, s2 ->
|
||||
Collator.getInstance(locale).compare(s1, s2)
|
||||
}.joinToString(", ").trim()
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.Network
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ResolvePromise
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
|
||||
db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
|
||||
rootId: String?, loadedProfiles: ReadableArray?
|
||||
): ReadableMap? {
|
||||
return try {
|
||||
val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val currentUser = find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val since = rootId?.let { queryLastPostInThread(db, it) }
|
||||
val queryParams = if (since == null) "?perPage=60&fromCreatedAt=0&direction=up" else
|
||||
"?fromCreateAt=${since.toLong()}&direction=down"
|
||||
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val since = queryPostSinceForChannel(db, channelId)
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val postData = postsResponse?.getMap("data")
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postData != null) {
|
||||
val data = ReadableMapUtils.toMap(postData)
|
||||
results.putMap("posts", postData)
|
||||
if (data != null) {
|
||||
val postsMap = data["posts"]
|
||||
if (postsMap != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
val userIdsAlreadyLoaded = mutableListOf<String>()
|
||||
if (loadedProfiles != null) {
|
||||
for (i in 0 until loadedProfiles.size()) {
|
||||
loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
while (iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
post?.let {
|
||||
val thread = Arguments.createMap()
|
||||
thread.putString("id", it.getString("id"))
|
||||
thread.putInt("reply_count", it.getInt("reply_count"))
|
||||
thread.putDouble("last_reply_at", 0.0)
|
||||
thread.putDouble("last_viewed_at", 0.0)
|
||||
thread.putArray("participants", it.getArray("participants"))
|
||||
thread.putMap("post", it)
|
||||
thread.putBoolean("is_following", try {
|
||||
it.getBoolean("is_following")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
})
|
||||
thread.putInt("unread_replies", 0)
|
||||
thread.putInt("unread_mentions", 0)
|
||||
thread.putDouble("delete_at", it.getDouble("delete_at"))
|
||||
threads.pushMap(thread)
|
||||
}
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post?.getArray("participants")
|
||||
participants?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
val participant = it.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach { (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findMyTeam
|
||||
import com.mattermost.helpers.database_extension.findTeam
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
|
||||
return try {
|
||||
var team: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
val teamExists = findTeam(db, teamId)
|
||||
val myTeamExists = findMyTeam(db, teamId)
|
||||
if (!teamExists) {
|
||||
team = fetch(serverUrl, "/api/v4/teams/$teamId")
|
||||
}
|
||||
|
||||
if (!myTeamExists) {
|
||||
myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me")
|
||||
}
|
||||
|
||||
Pair(team, myTeam)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: Database, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return null
|
||||
val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null
|
||||
|
||||
return try {
|
||||
val thread = fetch(serverUrl, "/api/v4/users/$currentUserId/teams/${threadTeamId}/threads/$threadId")
|
||||
thread?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(serverUrl: String, loadedUsers: ReadableArray?, data: ReadableMap?): ArrayList<Any> {
|
||||
val userList = ArrayList<Any>()
|
||||
loadedUsers?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
data?.getArray("userIdsToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersById(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usernamesToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersByUsernames(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usersFromThreads")?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
|
||||
return userList
|
||||
}
|
||||
|
||||
internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList<Any>) {
|
||||
for (i in 0 until users.size()) {
|
||||
list.add(users.getMap(i))
|
||||
}
|
||||
}
|
||||
@@ -7,29 +7,23 @@ import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
||||
import com.mattermost.helpers.DatabaseHelper;
|
||||
import com.mattermost.helpers.Network;
|
||||
import com.mattermost.helpers.NotificationHelper;
|
||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
||||
import com.mattermost.helpers.ReadableMapUtils;
|
||||
import com.mattermost.share.ShareModule;
|
||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
||||
|
||||
import static com.mattermost.helpers.database_extension.GeneralKt.*;
|
||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
||||
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private final PushNotificationDataHelper dataHelper;
|
||||
|
||||
@@ -57,6 +51,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
int notificationId = NotificationHelper.getNotificationId(initialData);
|
||||
|
||||
String serverUrl = addServerUrlToBundle(initialData);
|
||||
boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
if (ackId != null && serverUrl != null) {
|
||||
Bundle response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded);
|
||||
@@ -70,7 +65,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
finishProcessingNotification(serverUrl, type, channelId, notificationId);
|
||||
finishProcessingNotification(serverUrl, type, channelId, notificationId, isReactInit);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,9 +78,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
private void finishProcessingNotification(final String serverUrl, @NonNull final String type, final String channelId, final int notificationId) {
|
||||
final boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
|
||||
|
||||
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:
|
||||
@@ -97,17 +90,13 @@ public class CustomPushNotification extends PushNotification {
|
||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
||||
if (serverUrl != null) {
|
||||
if (serverUrl != null && !isReactInit) {
|
||||
// We will only fetch the data related to the notification on the native side
|
||||
// as updating the data directly to the db removes the wal & shm files needed
|
||||
// by watermelonDB, if the DB is updated while WDB is running it causes WDB to
|
||||
// detect the database as malformed, thus the app stop working and a restart is required.
|
||||
// Data will be fetch from within the JS context instead.
|
||||
Bundle notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit);
|
||||
if (notificationResult != null) {
|
||||
notificationBundle.putBundle("data", notificationResult);
|
||||
mNotificationProps = createProps(notificationBundle);
|
||||
}
|
||||
dataHelper.fetchAndStoreDataForPushNotification(notificationBundle);
|
||||
}
|
||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
||||
mContext,
|
||||
@@ -156,20 +145,17 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
private String addServerUrlToBundle(Bundle bundle) {
|
||||
DatabaseHelper dbHelper = DatabaseHelper.Companion.getInstance();
|
||||
String serverId = bundle.getString("server_id");
|
||||
String serverUrl = null;
|
||||
if (dbHelper != null) {
|
||||
if (serverId == null) {
|
||||
serverUrl = dbHelper.getOnlyServerUrl();
|
||||
} else {
|
||||
serverUrl = getServerUrlForIdentifier(dbHelper, serverId);
|
||||
}
|
||||
String serverUrl;
|
||||
if (serverId == null) {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getOnlyServerUrl();
|
||||
} else {
|
||||
serverUrl = Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).getServerUrlForIdentifier(serverId);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(serverUrl)) {
|
||||
bundle.putString("server_url", serverUrl);
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
if (!TextUtils.isEmpty(serverUrl)) {
|
||||
bundle.putString("server_url", serverUrl);
|
||||
mNotificationProps = createProps(bundle);
|
||||
}
|
||||
|
||||
return serverUrl;
|
||||
|
||||
@@ -13,12 +13,12 @@ class FoldableObserver(private val activity: Activity) {
|
||||
private var disposable: Disposable? = null
|
||||
private lateinit var observable: Observable<WindowLayoutInfo>
|
||||
|
||||
fun onCreate() {
|
||||
public fun onCreate() {
|
||||
observable = WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfoObservable(activity)
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
public fun onStart() {
|
||||
if (disposable?.isDisposed == true) {
|
||||
onCreate()
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class FoldableObserver(private val activity: Activity) {
|
||||
}
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
public fun onStop() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
@@ -14,11 +12,9 @@ import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
private final FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
private FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
@@ -34,7 +30,7 @@ public class MainActivity extends NavigationActivity {
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new DefaultReactActivityDelegate(
|
||||
this,
|
||||
Objects.requireNonNull(getMainComponentName()),
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled(), // fabricEnabled
|
||||
// If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18).
|
||||
@@ -63,7 +59,7 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
|
||||
@@ -101,7 +97,7 @@ public class MainActivity extends NavigationActivity {
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
private void setHWKeyboardConnected() {
|
||||
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;
|
||||
|
||||
@@ -32,10 +32,10 @@ import com.mattermost.helpers.RealPathUtil;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private static final String SAVE_EVENT = "MattermostManagedSaveFile";
|
||||
@@ -46,6 +46,8 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
private Promise mPickerPromise;
|
||||
private String fileContent;
|
||||
|
||||
private static final String TAG = MattermostManagedModule.class.getSimpleName();
|
||||
|
||||
private MattermostManagedModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.reactContext = reactContext;
|
||||
@@ -147,7 +149,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
try {
|
||||
final String packageName = currentActivity.getPackageName();
|
||||
final String authority = packageName + ".provider";
|
||||
final String authority = new StringBuilder(packageName).append(".provider").toString();
|
||||
contentUri = FileProvider.getUriForFile(currentActivity, authority, newFile);
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
@@ -174,7 +176,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_TITLE, filename);
|
||||
|
||||
PackageManager pm = Objects.requireNonNull(getCurrentActivity()).getPackageManager();
|
||||
PackageManager pm = getCurrentActivity().getPackageManager();
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
try {
|
||||
getCurrentActivity().startActivityForResult(intent, SAVE_REQUEST);
|
||||
@@ -209,7 +211,7 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
if (!TextUtils.isEmpty(token)) {
|
||||
WritableMap headers = Arguments.createMap();
|
||||
if (optionsMap.hasKey("headers")) {
|
||||
headers.merge(Objects.requireNonNull(optionsMap.getMap("headers")));
|
||||
headers.merge(optionsMap.getMap("headers"));
|
||||
}
|
||||
headers.putString("Authorization", "Bearer " + token);
|
||||
optionsMap.putMap("headers", headers);
|
||||
@@ -235,21 +237,34 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
|
||||
@Override
|
||||
protected Object doInBackgroundGuarded() {
|
||||
FileChannel source = null;
|
||||
FileChannel dest = null;
|
||||
try {
|
||||
ParcelFileDescriptor pfd = weakContext.get().getContentResolver().openFileDescriptor(toFile, "w");
|
||||
File input = new File(this.fromFile);
|
||||
try (FileInputStream fileInputStream = new FileInputStream(input)) {
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel source = fileInputStream.getChannel();
|
||||
FileChannel dest = fileOutputStream.getChannel();
|
||||
dest.transferFrom(source, 0, source.size());
|
||||
source.close();
|
||||
dest.close();
|
||||
}
|
||||
}
|
||||
pfd.close();
|
||||
FileInputStream fileInputStream = new FileInputStream(input);
|
||||
ParcelFileDescriptor pfd = weakContext.get().getContentResolver().openFileDescriptor(toFile, "w");
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
|
||||
source = fileInputStream.getChannel();
|
||||
dest = fileOutputStream.getChannel();
|
||||
dest.transferFrom(source, 0, source.size());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (source != null) {
|
||||
try {
|
||||
source.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (dest != null) {
|
||||
try {
|
||||
dest.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
@@ -29,10 +29,11 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
|
||||
|
||||
override fun getName() = "SplitView"
|
||||
|
||||
private fun sendEvent(params: WritableMap?) {
|
||||
fun sendEvent(eventName: String,
|
||||
params: WritableMap?) {
|
||||
reactContext
|
||||
.getJSModule(RCTDeviceEventEmitter::class.java)
|
||||
.emit("SplitViewChanged", params)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
|
||||
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
|
||||
@@ -50,7 +51,7 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
|
||||
fun setDeviceFolded(folded: Boolean) {
|
||||
val map = getSplitViewResults(folded)
|
||||
if (listenerCount > 0 && isDeviceFolded != folded) {
|
||||
sendEvent(map)
|
||||
sendEvent("SplitViewChanged", map)
|
||||
}
|
||||
isDeviceFolded = folded
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.json.JSONException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -76,8 +75,8 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
public String getCurrentActivityName() {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
if (currentActivity != null) {
|
||||
String activityName = currentActivity.getComponentName().getClassName();
|
||||
String[] components = activityName.split("\\.");
|
||||
String actvName = currentActivity.getComponentName().getClassName();
|
||||
String[] components = actvName.split("\\.");
|
||||
return components[components.length - 1];
|
||||
}
|
||||
|
||||
@@ -116,7 +115,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
if (data != null && data.hasKey("serverUrl")) {
|
||||
ReadableArray files = data.getArray("files");
|
||||
String serverUrl = data.getString("serverUrl");
|
||||
final String token = Credentials.getCredentialsForServerSync(mReactContext, serverUrl);
|
||||
final String token = Credentials.getCredentialsForServerSync(this.getReactApplicationContext(), serverUrl);
|
||||
JSONObject postData = buildPostObject(data);
|
||||
|
||||
if (files != null && files.size() > 0) {
|
||||
@@ -237,7 +236,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful()) {
|
||||
String responseData = Objects.requireNonNull(response.body()).string();
|
||||
String responseData = response.body().string();
|
||||
JSONObject responseJson = new JSONObject(responseData);
|
||||
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
|
||||
JSONArray file_ids = new JSONArray();
|
||||
|
||||
@@ -7,9 +7,9 @@ buildscript {
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
supportLibVersion = "33.0.0"
|
||||
kotlinVersion = "1.7.21"
|
||||
kotlin_version = "1.7.21"
|
||||
firebaseVersion = "23.1.1"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlin_version = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
@@ -23,7 +23,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath('com.google.gms:google-services:4.3.15')
|
||||
classpath('com.google.gms:google-services:4.3.14')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -29,7 +29,7 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.177.0
|
||||
FLIPPER_VERSION=0.125.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
|
||||
@@ -5,8 +5,6 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
include ':watermelondb'
|
||||
project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')
|
||||
include ':watermelondb-jsi'
|
||||
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
58
app/actions/local/notification.ts
Normal file
58
app/actions/local/notification.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, queryPostsInChannel, queryPostsInThread} from '@queries/servers/post';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const updatePostSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (notification.payload?.channel_id) {
|
||||
const chunks = await queryPostsInChannel(database, notification.payload.channel_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed updatePostSinceCache', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePostsInThreadsSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
if (notification.payload?.root_id) {
|
||||
const {database} = operator;
|
||||
const chunks = await queryPostsInThread(database, notification.payload.root_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
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';
|
||||
@@ -726,15 +726,11 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
|
||||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false) {
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.viewMyChannel(channelId);
|
||||
|
||||
if (updateLocal) {
|
||||
await markChannelAsViewed(serverUrl, channelId, true);
|
||||
}
|
||||
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
|
||||
@@ -1,38 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logInfo} from '@utils/log';
|
||||
|
||||
import {verifyPushProxy} from './common';
|
||||
import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
export async function appEntry(serverUrl: string, since = 0) {
|
||||
export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
if (!since) {
|
||||
registerDeviceToken(serverUrl);
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Run data retention cleanup
|
||||
await dataRetentionCleanup(serverUrl);
|
||||
|
||||
// clear lastUnreadChannelId
|
||||
const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''});
|
||||
if (removeLastUnreadChannelId) {
|
||||
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
|
||||
}
|
||||
|
||||
WebsocketManager.openAll();
|
||||
const {database} = operator;
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since;
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData;
|
||||
if (isUpgrade && meData?.user) {
|
||||
const isTabletDevice = await isTablet();
|
||||
const me = await prepareCommonSystemValues(operator, {
|
||||
currentUserId: meData.user.id,
|
||||
currentTeamId: initialTeamId,
|
||||
currentChannelId: isTabletDevice ? initialChannelId : undefined,
|
||||
});
|
||||
if (me?.length) {
|
||||
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, 'appEntry');
|
||||
logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
return {};
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
|
||||
export async function upgradeEntry(serverUrl: string) {
|
||||
@@ -40,7 +89,7 @@ export async function upgradeEntry(serverUrl: string) {
|
||||
|
||||
try {
|
||||
const configAndLicense = await fetchConfigAndLicense(serverUrl, false);
|
||||
const entryData = await appEntry(serverUrl, 0);
|
||||
const entryData = await appEntry(serverUrl, 0, true);
|
||||
const error = configAndLicense.error || entryData.error;
|
||||
|
||||
if (!error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {dataRetentionCleanup} from '@actions/local/systems';
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
@@ -9,7 +10,8 @@ import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlAllChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
@@ -19,13 +21,16 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
@@ -106,12 +111,6 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?:
|
||||
}
|
||||
|
||||
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData;
|
||||
const chError = chData?.error as ClientError | undefined;
|
||||
if (chError?.status_code === 403) {
|
||||
// if the user does not have appropriate permissions, which means the user those not belong to the team,
|
||||
// we set it as there is no errors, so that the teams and others can be properly handled
|
||||
chData!.error = undefined;
|
||||
}
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
if (error) {
|
||||
return {error};
|
||||
@@ -312,8 +311,13 @@ export async function restDeferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string) {
|
||||
// defer sidebar DM & GM profiles
|
||||
let channelsToFetchProfiles: Set<Channel>|undefined;
|
||||
setTimeout(async () => {
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
const directChannels = chData.channels.filter(isDMorGM);
|
||||
channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
|
||||
}
|
||||
@@ -345,11 +349,8 @@ export async function restDeferredAppEntryActions(
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId);
|
||||
|
||||
// defer sidebar DM & GM profiles
|
||||
setTimeout(async () => {
|
||||
const directChannels = chData?.channels?.filter(isDMorGM);
|
||||
const channelsToFetchProfiles = new Set<Channel>(directChannels);
|
||||
if (channelsToFetchProfiles.size) {
|
||||
if (channelsToFetchProfiles?.size) {
|
||||
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
|
||||
}
|
||||
@@ -372,6 +373,107 @@ export const registerDeviceToken = async (serverUrl: string) => {
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const syncOtherServers = async (serverUrl: string) => {
|
||||
const servers = await getAllServers();
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
registerDeviceToken(server.url);
|
||||
syncAllChannelMembersAndThreads(server.url).then(() => {
|
||||
dataRetentionCleanup(server.url);
|
||||
});
|
||||
autoUpdateTimezone(server.url);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllChannelMembersAndThreads = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (config?.FeatureFlagGraphQL === 'true') {
|
||||
const error = await graphQLSyncAllChannelMembers(serverUrl);
|
||||
if (error) {
|
||||
logDebug('failed graphQL, falling back to rest', error);
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
} else {
|
||||
restSyncAllChannelMembers(serverUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const graphQLSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return 'Server database not found';
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const response = await gqlAllChannels(serverUrl);
|
||||
if ('error' in response) {
|
||||
return response.error;
|
||||
}
|
||||
|
||||
if (response.errors) {
|
||||
return response.errors[0].message;
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
const channels = getMemberChannelsFromGQLQuery(response.data);
|
||||
const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId));
|
||||
|
||||
if (channels && memberships) {
|
||||
const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true);
|
||||
const models = (await Promise.all(modelPromises)).flat();
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'graphQLSyncAllChannelMembers');
|
||||
}
|
||||
}
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
if (isCRTEnabled) {
|
||||
const myTeams = await queryMyTeams(operator.database).fetch();
|
||||
for await (const myTeam of myTeams) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, myTeam.id);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const restSyncAllChannelMembers = async (serverUrl: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const myTeams = await client.getMyTeams();
|
||||
const preferences = await client.getMyPreferences();
|
||||
const config = await client.getClientConfigOld();
|
||||
|
||||
let excludeDirect = false;
|
||||
for await (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
|
||||
// need to await here since GM/DM threads in different teams overlap
|
||||
await syncTeamThreads(serverUrl, myTeam.id);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
export async function verifyPushProxy(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
@@ -436,14 +538,7 @@ export async function handleEntryAfterLoadNavigation(
|
||||
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
|
||||
const tabletDevice = await isTablet();
|
||||
|
||||
if (!currentTeamIdAfterLoad) {
|
||||
// First load or no team
|
||||
if (tabletDevice) {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId);
|
||||
} else {
|
||||
await setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
} else if (currentTeamIdAfterLoad !== currentTeamId) {
|
||||
if (currentTeamIdAfterLoad !== currentTeamId) {
|
||||
// Switched teams while loading
|
||||
if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) {
|
||||
await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad);
|
||||
|
||||
@@ -7,9 +7,9 @@ import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {autoUpdateTimezone, fetchProfilesInGroupChannels, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {autoUpdateTimezone, updateAllUsersSince} from '@actions/remote/user';
|
||||
import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry';
|
||||
import {General, Preferences} from '@constants';
|
||||
import {Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
@@ -28,8 +28,6 @@ import type ClientError from '@client/rest/error';
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
const FETCH_MISSING_GM_TIMEOUT = 2500;
|
||||
|
||||
export async function deferredAppEntryGraphQLActions(
|
||||
serverUrl: string,
|
||||
since: number,
|
||||
@@ -100,19 +98,6 @@ export async function deferredAppEntryGraphQLActions(
|
||||
updateCanJoinTeams(serverUrl);
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
|
||||
// defer sidebar GM profiles
|
||||
setTimeout(async () => {
|
||||
const gmIds = chData?.channels?.reduce<Set<string>>((acc, v) => {
|
||||
if (v?.type === General.GM_CHANNEL) {
|
||||
acc.add(v.id);
|
||||
}
|
||||
return acc;
|
||||
}, new Set<string>());
|
||||
if (gmIds?.size) {
|
||||
fetchProfilesInGroupChannels(serverUrl, Array.from(gmIds));
|
||||
}
|
||||
}, FETCH_MISSING_GM_TIMEOUT);
|
||||
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
type AfterLoginArgs = {
|
||||
serverUrl: string;
|
||||
user: UserProfile;
|
||||
deviceToken?: string;
|
||||
}
|
||||
|
||||
export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: any}> {
|
||||
export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> {
|
||||
const dt = Date.now();
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (deviceToken) {
|
||||
try {
|
||||
client.attachDevice(deviceToken);
|
||||
} catch {
|
||||
// do nothing, the token could've failed to attach to the session but is not a blocker
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const clData = await fetchConfigAndLicense(serverUrl, false);
|
||||
if (clData.error) {
|
||||
return {error: clData.error};
|
||||
}
|
||||
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
await WebsocketManager.initializeClient(serverUrl);
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, '', '');
|
||||
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
|
||||
return {};
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
|
||||
let switchToChannel = false;
|
||||
if (initialChannelId && isTabletDevice) {
|
||||
switchToChannel = true;
|
||||
switchToChannelById(serverUrl, initialChannelId, initialTeamId);
|
||||
} else {
|
||||
setCurrentTeamAndChannelId(operator, initialTeamId, '');
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'loginEntry');
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const config = clData.config || {} as ClientConfig;
|
||||
const license = clData.license || {} as ClientLicense;
|
||||
deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined);
|
||||
|
||||
return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostById} from '@actions/remote/post';
|
||||
import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {Screens} from '@constants';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getMyChannel} from '@queries/servers/channel';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {queryThemePreferences} from '@queries/servers/preference';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {setTeamLoading} from '@store/team_load_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme';
|
||||
|
||||
import type ClientError from '@client/rest/error';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import {syncOtherServers} from './common';
|
||||
import {deferredAppEntryActions, entry} from './gql_common';
|
||||
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData) {
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
// We only reach this point if we have a channel Id in the notification payload
|
||||
const channelId = notification.channel_id!;
|
||||
const rootId = notification.root_id!;
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const rootId = notification.payload!.root_id!;
|
||||
const {database} = operator;
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
|
||||
let teamId = notification.team_id;
|
||||
let isDirectChannel = false;
|
||||
|
||||
let teamId = notification.payload?.team_id;
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
isDirectChannel = true;
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
|
||||
@@ -58,62 +61,91 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
||||
updateThemeIfNeeded(theme, true);
|
||||
}
|
||||
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
|
||||
// To make the switch faster we determine if we already have the team & channel
|
||||
let myChannel: MyChannelModel | ChannelMembership | undefined = await getMyChannel(database, channelId);
|
||||
let myTeam: MyTeamModel | TeamMembership | undefined = await getMyTeamById(database, teamId);
|
||||
|
||||
if (!myTeam) {
|
||||
const resp = await fetchMyTeam(serverUrl, teamId);
|
||||
if (resp.error) {
|
||||
if ((resp.error as ClientError).status_code === 403) {
|
||||
emitNotificationError('Team');
|
||||
} else {
|
||||
emitNotificationError('Connection');
|
||||
}
|
||||
} else {
|
||||
myTeam = resp.memberships?.[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!myChannel) {
|
||||
const resp = await fetchMyChannel(serverUrl, teamId, channelId);
|
||||
if (resp.error) {
|
||||
if ((resp.error as ClientError).status_code === 403) {
|
||||
emitNotificationError('Channel');
|
||||
} else {
|
||||
emitNotificationError('Connection');
|
||||
}
|
||||
} else {
|
||||
myChannel = resp.memberships?.[0];
|
||||
}
|
||||
}
|
||||
const myChannel = await getMyChannel(database, channelId);
|
||||
const myTeam = await getMyTeamById(database, teamId);
|
||||
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(rootId);
|
||||
|
||||
let switchedToScreen = false;
|
||||
let switchedToChannel = false;
|
||||
if (myChannel && myTeam) {
|
||||
if (isThreadNotification) {
|
||||
let post: PostModel | Post | undefined = await getPostById(database, rootId);
|
||||
if (!post) {
|
||||
const resp = await fetchPostById(serverUrl, rootId);
|
||||
post = resp.post;
|
||||
}
|
||||
|
||||
const actualRootId = post && ('root_id' in post ? post.root_id : post.rootId);
|
||||
|
||||
if (actualRootId) {
|
||||
await fetchAndSwitchToThread(serverUrl, actualRootId, true);
|
||||
} else if (post) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
emitNotificationError('Post');
|
||||
}
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
switchedToScreen = true;
|
||||
}
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, teamId, channelId);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return {error: entryData.error};
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
// There is a chance that after the above request returns
|
||||
// the user is no longer part of the team or channel
|
||||
// that triggered the notification (rare but possible)
|
||||
let selectedTeamId = teamId;
|
||||
let selectedChannelId = channelId;
|
||||
if (initialTeamId !== teamId) {
|
||||
// We are no longer a part of the team that the notification belongs to
|
||||
// Immediately set the new team as the current team in the database so that the UI
|
||||
// renders the correct team.
|
||||
selectedTeamId = initialTeamId;
|
||||
if (!isDirectChannel) {
|
||||
selectedChannelId = initialChannelId;
|
||||
}
|
||||
}
|
||||
|
||||
WebsocketManager.openAll();
|
||||
if (!switchedToScreen) {
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isTabletDevice || (channelId === selectedChannelId)) {
|
||||
// Make switch again to get the missing data and make sure the team is the correct one
|
||||
switchedToScreen = true;
|
||||
if (isThreadNotification) {
|
||||
await fetchAndSwitchToThread(serverUrl, rootId, true);
|
||||
} else {
|
||||
switchedToChannel = true;
|
||||
await switchToChannelById(serverUrl, channelId, teamId);
|
||||
}
|
||||
} else if (teamId !== selectedTeamId || channelId !== selectedChannelId) {
|
||||
// If in the end the selected team or channel is different than the one from the notification
|
||||
// we switch again
|
||||
await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
if (teamId !== selectedTeamId) {
|
||||
emitNotificationError('Team');
|
||||
} else if (channelId !== selectedChannelId) {
|
||||
emitNotificationError('Channel');
|
||||
}
|
||||
|
||||
// Waiting for the screen to display fixes a race condition when fetching and storing data
|
||||
if (switchedToChannel) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL);
|
||||
} else if (switchedToScreen && isThreadNotification) {
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'pushNotificationEntry');
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!;
|
||||
const config = await getConfig(database);
|
||||
const license = await getLicense(database);
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
|
||||
syncOtherServers(serverUrl);
|
||||
|
||||
return {userId: currentUserId};
|
||||
}
|
||||
|
||||
@@ -3,26 +3,19 @@
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, storeCategories} from '@actions/local/category';
|
||||
import {storeMyChannelsForTeam} from '@actions/local/channel';
|
||||
import {storePostsForChannel} from '@actions/local/post';
|
||||
// import {updatePostSinceCache, updatePostsInThreadsSinceCache} from '@actions/local/notification';
|
||||
import {fetchDirectChannelsInfo, fetchMyChannel, switchToChannelById} from '@actions/remote/channel';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {fetchMyTeam} from '@actions/remote/team';
|
||||
import {fetchAndSwitchToThread} from '@actions/remote/thread';
|
||||
import {ActionType} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getMyChannel, getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import {getMyTeamById, prepareMyTeams} from '@queries/servers/team';
|
||||
import {getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {getMyTeamById} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning} from '@utils/log';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
import {processPostsFetched} from '@utils/post';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
const fetchNotificationData = async (serverUrl: string, notification: NotificationWithData, skipEvents = false) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -102,84 +95,24 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
};
|
||||
|
||||
export const backgroundNotification = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channelId = notification.payload?.channel_id;
|
||||
let teamId = notification.payload?.team_id;
|
||||
if (!channelId) {
|
||||
throw new Error('No chanel Id was specified');
|
||||
}
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
// If the notification payload does not have a teamId we assume is a DM/GM
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
teamId = currentTeamId;
|
||||
}
|
||||
if (notification.payload?.data) {
|
||||
const {data, isCRTEnabled} = notification.payload;
|
||||
const {channel, myChannel, team, myTeam, posts, users, threads} = data;
|
||||
const models: Model[] = [];
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
if (lastDisconnectedAt) {
|
||||
// if (Platform.OS === 'ios') {
|
||||
// const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
// const isThreadNotification = isCRTEnabled && Boolean(notification.payload?.root_id);
|
||||
// if (isThreadNotification) {
|
||||
// updatePostsInThreadsSinceCache(serverUrl, notification);
|
||||
// } else {
|
||||
// updatePostSinceCache(serverUrl, notification);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (posts) {
|
||||
const postsData = processPostsFetched(posts);
|
||||
const isThreadNotification = isCRTEnabled && Boolean(notification.payload.root_id);
|
||||
const actionType = isThreadNotification ? ActionType.POSTS.RECEIVED_IN_THREAD : ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
|
||||
if (team || myTeam) {
|
||||
const teamPromises = prepareMyTeams(operator, team ? [team] : [], myTeam ? [myTeam] : []);
|
||||
if (teamPromises.length) {
|
||||
const teamModels = await Promise.all(teamPromises);
|
||||
models.push(...teamModels.flat());
|
||||
}
|
||||
}
|
||||
|
||||
await storeMyChannelsForTeam(
|
||||
serverUrl, teamId,
|
||||
channel ? [channel] : [],
|
||||
myChannel ? [myChannel] : [],
|
||||
false, isCRTEnabled,
|
||||
);
|
||||
|
||||
if (data.categoryChannels?.length && channel) {
|
||||
const {models: categoryModels} = await addChannelToDefaultCategory(serverUrl, channel, true);
|
||||
if (categoryModels?.length) {
|
||||
models.push(...categoryModels);
|
||||
}
|
||||
} else if (data.categories?.categories) {
|
||||
const {models: categoryModels} = await storeCategories(serverUrl, data.categories.categories, false, true);
|
||||
if (categoryModels?.length) {
|
||||
models.push(...categoryModels);
|
||||
}
|
||||
}
|
||||
|
||||
await storePostsForChannel(
|
||||
serverUrl, channelId,
|
||||
postsData.posts, postsData.order, postsData.previousPostId ?? '',
|
||||
actionType, users || [],
|
||||
);
|
||||
|
||||
if (isThreadNotification && threads?.length) {
|
||||
const threadModels = await operator.handleThreads({
|
||||
threads: threads.map((t) => ({
|
||||
...t,
|
||||
lastFetchedAt: Math.max(t.post.create_at, t.post.update_at, t.post.delete_at),
|
||||
})),
|
||||
teamId,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
|
||||
if (threadModels.length) {
|
||||
models.push(...threadModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'backgroundNotification');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('backgroundNotification', error);
|
||||
await fetchNotificationData(serverUrl, notification, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {handleReconnect} from '@actions/websocket';
|
||||
import {Events, General, Preferences} from '@constants';
|
||||
import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {querySavedPostsPreferences} from '@queries/servers/preference';
|
||||
import {getCurrentUserId} from '@queries/servers/system';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
@@ -190,11 +185,3 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const handleCRTToggled = async (serverUrl: string) => {
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
await truncateCrtRelatedTables(serverUrl);
|
||||
await handleReconnect(serverUrl);
|
||||
EphemeralStore.setEnablingCRT(false);
|
||||
DeviceEventEmitter.emit(Events.CRT_TOGGLED, serverUrl === currentServerUrl);
|
||||
};
|
||||
|
||||
@@ -7,14 +7,16 @@ import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
import {Database, Events} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getServerDisplayName} from '@queries/app/servers';
|
||||
import {getCurrentUserId, getExpiredSession} from '@queries/servers/system';
|
||||
import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {logWarning, logError, logDebug} from '@utils/log';
|
||||
import {logWarning, logError} from '@utils/log';
|
||||
import {scheduleExpiredNotification} from '@utils/notification';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
@@ -25,25 +27,47 @@ import type {LoginArgs} from '@typings/database/database';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [];
|
||||
|
||||
// Set push proxy verification
|
||||
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
|
||||
if (ppVerification) {
|
||||
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
await operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
} catch (error) {
|
||||
logDebug('error setting the push proxy verification state on login', error);
|
||||
export const completeLogin = async (serverUrl: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const license = await getLicense(database);
|
||||
const config = await getConfig(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
|
||||
const systems: IdValue[] = [];
|
||||
|
||||
// Set push proxy verification
|
||||
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
|
||||
if (ppVerification) {
|
||||
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
|
||||
}
|
||||
|
||||
// Start websocket
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (systems.length) {
|
||||
operator.handleSystem({systems, prepareRecordsOnly: false});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
@@ -127,12 +151,11 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
}
|
||||
|
||||
try {
|
||||
await addPushProxyVerificationStateFromLogin(serverUrl);
|
||||
const {error} = await loginEntry({serverUrl});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
return {error: error as ClientError, failed: false};
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
|
||||
completeLogin(serverUrl);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false};
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -222,12 +245,13 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) =
|
||||
return {error};
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
error: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
|
||||
let deviceToken;
|
||||
let user;
|
||||
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
@@ -255,6 +279,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
displayName: serverDisplayName,
|
||||
},
|
||||
});
|
||||
deviceToken = await getDeviceToken();
|
||||
user = await client.getMe();
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
@@ -269,12 +294,11 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
}
|
||||
|
||||
try {
|
||||
await addPushProxyVerificationStateFromLogin(serverUrl);
|
||||
const {error} = await loginEntry({serverUrl});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
return {error: error as ClientError, failed: false};
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
|
||||
completeLogin(serverUrl);
|
||||
return {error: error as ClientError, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error: error as ClientError, failed: false};
|
||||
return {error: error as ClientError, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ export const updateTeamThreadsAsRead = async (serverUrl: string, teamId: string)
|
||||
}
|
||||
};
|
||||
|
||||
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string, updateLastViewed = true) => {
|
||||
export const markThreadAsRead = async (serverUrl: string, teamId: string | undefined, threadId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
|
||||
if (!database) {
|
||||
@@ -141,7 +141,7 @@ export const markThreadAsRead = async (serverUrl: string, teamId: string | undef
|
||||
|
||||
// Update locally
|
||||
await updateThread(serverUrl, threadId, {
|
||||
last_viewed_at: updateLastViewed ? timestamp : undefined,
|
||||
last_viewed_at: timestamp,
|
||||
unread_replies: 0,
|
||||
unread_mentions: 0,
|
||||
});
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {markChannelAsViewed} from '@actions/local/channel';
|
||||
import {dataRetentionCleanup} from '@actions/local/systems';
|
||||
import {markChannelAsRead} from '@actions/remote/channel';
|
||||
import {handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common';
|
||||
import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common';
|
||||
import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common';
|
||||
import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post';
|
||||
import {autoUpdateTimezone} from '@actions/remote/user';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
import {loadConfigAndCalls} from '@calls/actions/calls';
|
||||
import {
|
||||
handleCallChannelDisabled,
|
||||
@@ -33,15 +32,17 @@ import {Screens, WebsocketEvents} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import AppsManager from '@managers/apps_manager';
|
||||
import {getCurrentChannel} from '@queries/servers/channel';
|
||||
import {getLastPostInThread} from '@queries/servers/post';
|
||||
import {
|
||||
getConfig,
|
||||
getCurrentChannelId,
|
||||
getCurrentTeamId,
|
||||
getCurrentUserId,
|
||||
getLicense,
|
||||
getWebSocketLastDisconnected,
|
||||
resetWebSocketLastDisconnected,
|
||||
} from '@queries/servers/system';
|
||||
import {getCurrentTeam} from '@queries/servers/team';
|
||||
import {getIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
@@ -71,14 +72,36 @@ import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent,
|
||||
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
|
||||
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
|
||||
|
||||
// ESR: 5.37
|
||||
const alreadyConnected = new Set<string>();
|
||||
|
||||
export async function handleFirstConnect(serverUrl: string) {
|
||||
registerDeviceToken(serverUrl);
|
||||
autoUpdateTimezone(serverUrl);
|
||||
return doReconnect(serverUrl);
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
const {database} = operator;
|
||||
const config = await getConfig(database);
|
||||
const lastDisconnect = await getWebSocketLastDisconnected(database);
|
||||
|
||||
// ESR: 5.37
|
||||
if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) {
|
||||
await handleReconnect(serverUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
alreadyConnected.add(serverUrl);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
fetchStatusByIds(serverUrl, ['me']);
|
||||
|
||||
if (isSupportedServerCalls(config?.Version)) {
|
||||
const currentUserId = await getCurrentUserId(database);
|
||||
loadConfigAndCalls(serverUrl, currentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleReconnect(serverUrl: string) {
|
||||
return doReconnect(serverUrl);
|
||||
await doReconnect(serverUrl);
|
||||
}
|
||||
|
||||
export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
@@ -100,12 +123,12 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) {
|
||||
async function doReconnect(serverUrl: string) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return new Error('cannot find server database');
|
||||
return;
|
||||
}
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return new Error('cannot find app database');
|
||||
return;
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
@@ -113,30 +136,21 @@ async function doReconnect(serverUrl: string) {
|
||||
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
|
||||
resetWebSocketLastDisconnected(operator);
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
const currentChannelId = await getCurrentChannelId(database);
|
||||
const currentTeam = await getCurrentTeam(database);
|
||||
const currentChannel = await getCurrentChannel(database);
|
||||
|
||||
setTeamLoading(serverUrl, true);
|
||||
const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastDisconnectedAt);
|
||||
const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt);
|
||||
if ('error' in entryData) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
return entryData.error;
|
||||
return;
|
||||
}
|
||||
const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData;
|
||||
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId);
|
||||
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId);
|
||||
|
||||
const dt = Date.now();
|
||||
if (models?.length) {
|
||||
await operator.batchRecords(models, 'doReconnect');
|
||||
}
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
if (tabletDevice && initialChannelId === currentChannelId) {
|
||||
await markChannelAsRead(serverUrl, initialChannelId);
|
||||
markChannelAsViewed(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
await operator.batchRecords(models, 'doReconnect');
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
@@ -152,10 +166,7 @@ async function doReconnect(serverUrl: string) {
|
||||
|
||||
await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
dataRetentionCleanup(serverUrl);
|
||||
|
||||
AppsManager.refreshAppBindings(serverUrl);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
@@ -398,13 +409,6 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
break;
|
||||
case WebsocketEvents.GROUP_DISSOCIATED_TO_CHANNEL:
|
||||
break;
|
||||
|
||||
// Plugins
|
||||
case WebsocketEvents.PLUGIN_STATUSES_CHANGED:
|
||||
case WebsocketEvents.PLUGIN_ENABLED:
|
||||
case WebsocketEvents.PLUGIN_DISABLED:
|
||||
// Do nothing, this event doesn't need logic in the mobile app
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
import {storeMyChannelsForTeam, markChannelAsUnread, markChannelAsViewed, updateLastPostAt} from '@actions/local/channel';
|
||||
import {markPostAsDeleted} from '@actions/local/post';
|
||||
import {createThreadFromNewPost, updateThread} from '@actions/local/thread';
|
||||
import {fetchChannelStats, fetchMyChannel} from '@actions/remote/channel';
|
||||
import {fetchChannelStats, fetchMyChannel, markChannelAsRead} from '@actions/remote/channel';
|
||||
import {fetchPostAuthors, fetchPostById} from '@actions/remote/post';
|
||||
import {fetchThread} from '@actions/remote/thread';
|
||||
import {ActionType, Events, Screens} from '@constants';
|
||||
@@ -116,6 +116,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
|
||||
if (!shouldIgnorePost(post)) {
|
||||
let markAsViewed = false;
|
||||
let markAsRead = false;
|
||||
|
||||
if (!myChannel.manuallyUnread) {
|
||||
if (
|
||||
@@ -124,17 +125,21 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
!isFromWebhook(post)
|
||||
) {
|
||||
markAsViewed = true;
|
||||
markAsRead = false;
|
||||
} else if ((post.channel_id === currentChannelId)) {
|
||||
const isChannelScreenMounted = NavigationStore.getScreensInStack().includes(Screens.CHANNEL);
|
||||
|
||||
const isTabletDevice = await isTablet();
|
||||
if (isChannelScreenMounted || isTabletDevice) {
|
||||
markAsViewed = false;
|
||||
markAsRead = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (markAsViewed) {
|
||||
if (markAsRead) {
|
||||
markChannelAsRead(serverUrl, post.channel_id);
|
||||
} else if (markAsViewed) {
|
||||
preparedMyChannelHack(myChannel);
|
||||
const {member: viewedAt} = await markChannelAsViewed(serverUrl, post.channel_id, false, true);
|
||||
if (viewedAt) {
|
||||
|
||||
@@ -1,32 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {updateDmGmDisplayName} from '@actions/local/channel';
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
import {fetchPostById} from '@actions/remote/post';
|
||||
import {handleCRTToggled} from '@actions/remote/preference';
|
||||
import {Preferences} from '@constants';
|
||||
import {Events, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getPostById} from '@queries/servers/post';
|
||||
import {deletePreferences, differsFromLocalNameFormat, getHasCRTChanged} from '@queries/servers/preference';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
async function handleCRTToggled(serverUrl: string) {
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
await truncateCrtRelatedTables(serverUrl);
|
||||
appEntry(serverUrl);
|
||||
DeviceEventEmitter.emit(Events.CRT_TOGGLED, serverUrl === currentServerUrl);
|
||||
}
|
||||
|
||||
export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
if (EphemeralStore.isEnablingCRT()) {
|
||||
let database;
|
||||
let operator;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
database = result.database;
|
||||
operator = result.operator;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const preference: PreferenceType = JSON.parse(msg.data.preference);
|
||||
handleSavePostAdded(serverUrl, [preference]);
|
||||
|
||||
const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, [preference]);
|
||||
const crtToggled = await getHasCRTChanged(database, [preference]);
|
||||
|
||||
await operator.handlePreferences({
|
||||
prepareRecordsOnly: false,
|
||||
preferences: [preference],
|
||||
});
|
||||
if (operator) {
|
||||
await operator.handlePreferences({
|
||||
prepareRecordsOnly: false,
|
||||
preferences: [preference],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDiffNameFormatPref) {
|
||||
updateDmGmDisplayName(serverUrl);
|
||||
@@ -41,22 +57,22 @@ export async function handlePreferenceChangedEvent(serverUrl: string, msg: WebSo
|
||||
}
|
||||
|
||||
export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
if (EphemeralStore.isEnablingCRT()) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
||||
handleSavePostAdded(serverUrl, preferences);
|
||||
|
||||
const hasDiffNameFormatPref = await differsFromLocalNameFormat(database, preferences);
|
||||
const crtToggled = await getHasCRTChanged(database, preferences);
|
||||
|
||||
await operator.handlePreferences({
|
||||
prepareRecordsOnly: false,
|
||||
preferences,
|
||||
});
|
||||
const hasDiffNameFormatPref = await differsFromLocalNameFormat(operator.database, preferences);
|
||||
const crtToggled = await getHasCRTChanged(operator.database, preferences);
|
||||
if (operator) {
|
||||
await operator.handlePreferences({
|
||||
prepareRecordsOnly: false,
|
||||
preferences,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasDiffNameFormatPref) {
|
||||
updateDmGmDisplayName(serverUrl);
|
||||
@@ -71,10 +87,14 @@ export async function handlePreferencesChangedEvent(serverUrl: string, msg: WebS
|
||||
}
|
||||
|
||||
export async function handlePreferencesDeletedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl];
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const databaseAndOperator = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const preferences: PreferenceType[] = JSON.parse(msg.data.preferences);
|
||||
deletePreferences(databaseAndOperator, preferences);
|
||||
deletePreferences(database, preferences);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
@@ -82,17 +102,16 @@ export async function handlePreferencesDeletedEvent(serverUrl: string, msg: WebS
|
||||
|
||||
// If preferences include new save posts we fetch them
|
||||
async function handleSavePostAdded(serverUrl: string, preferences: PreferenceType[]) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const savedPosts = preferences.filter((p) => p.category === Preferences.CATEGORIES.SAVED_POST);
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
for await (const saved of savedPosts) {
|
||||
const post = await getPostById(database, saved.name);
|
||||
if (!post) {
|
||||
await fetchPostById(serverUrl, saved.name, false);
|
||||
}
|
||||
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) {
|
||||
await fetchPostById(serverUrl, saved.name, false);
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread';
|
||||
import {getCurrentTeamId} from '@app/queries/servers/system';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getCurrentTeamId} from '@queries/servers/system';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise<void> {
|
||||
|
||||
@@ -6,19 +6,17 @@ import {Platform} from 'react-native';
|
||||
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getConfigValue} from '@queries/servers/system';
|
||||
import {hasReliableWebsocket} from '@utils/config';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {getConfig} from '@queries/servers/system';
|
||||
import {logError, logInfo, logWarning} from '@utils/log';
|
||||
|
||||
const MAX_WEBSOCKET_FAILS = 7;
|
||||
const WEBSOCKET_TIMEOUT = toMilliseconds({seconds: 30});
|
||||
const MIN_WEBSOCKET_RETRY_TIME = toMilliseconds({seconds: 3});
|
||||
const MAX_WEBSOCKET_RETRY_TIME = toMilliseconds({minutes: 5});
|
||||
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
|
||||
|
||||
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
|
||||
|
||||
export default class WebSocketClient {
|
||||
private conn?: WebSocketClientInterface;
|
||||
private connectionTimeout: NodeJS.Timeout | undefined;
|
||||
private connectionTimeout: any;
|
||||
private connectionId: string;
|
||||
private token: string;
|
||||
|
||||
@@ -45,7 +43,6 @@ export default class WebSocketClient {
|
||||
private url = '';
|
||||
|
||||
private serverUrl: string;
|
||||
private hasReliablyReconnect = false;
|
||||
|
||||
constructor(serverUrl: string, token: string, lastDisconnect = 0) {
|
||||
this.connectionId = '';
|
||||
@@ -79,12 +76,8 @@ export default class WebSocketClient {
|
||||
return;
|
||||
}
|
||||
|
||||
const [websocketUrl, version, reliableWebsocketConfig] = await Promise.all([
|
||||
getConfigValue(database, 'WebsocketURL'),
|
||||
getConfigValue(database, 'Version'),
|
||||
getConfigValue(database, 'EnableReliableWebSockets'),
|
||||
]);
|
||||
const connectionUrl = (websocketUrl || this.serverUrl) + '/api/v4/websocket';
|
||||
const config = await getConfig(database);
|
||||
const connectionUrl = (config.WebsocketURL || this.serverUrl) + '/api/v4/websocket';
|
||||
|
||||
if (this.connectingCallback) {
|
||||
this.connectingCallback();
|
||||
@@ -105,7 +98,7 @@ export default class WebSocketClient {
|
||||
|
||||
this.url = connectionUrl;
|
||||
|
||||
const reliableWebSockets = hasReliableWebsocket(version, reliableWebsocketConfig);
|
||||
const reliableWebSockets = config.EnableReliableWebSockets === 'true';
|
||||
if (reliableWebSockets) {
|
||||
// Add connection id, and last_sequence_number to the query param.
|
||||
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
|
||||
@@ -132,12 +125,7 @@ export default class WebSocketClient {
|
||||
// iOS is using he underlying cookieJar
|
||||
headers.Authorization = `Bearer ${this.token}`;
|
||||
}
|
||||
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT});
|
||||
|
||||
// Check again if the client is the same, to avoid race conditions
|
||||
if (this.conn === client) {
|
||||
return;
|
||||
}
|
||||
const {client} = await getOrCreateWebSocketClient(this.url, {headers});
|
||||
this.conn = client;
|
||||
} catch (error) {
|
||||
return;
|
||||
@@ -166,7 +154,6 @@ export default class WebSocketClient {
|
||||
if (this.serverSequence && this.missedEventsCallback) {
|
||||
this.missedEventsCallback();
|
||||
}
|
||||
this.hasReliablyReconnect = true;
|
||||
}
|
||||
} else if (this.firstConnectCallback) {
|
||||
logInfo('websocket connected to', this.url);
|
||||
@@ -184,7 +171,6 @@ export default class WebSocketClient {
|
||||
|
||||
this.conn = undefined;
|
||||
this.responseSequence = 1;
|
||||
this.hasReliablyReconnect = false;
|
||||
|
||||
if (this.connectFailCount === 0) {
|
||||
logInfo('websocket closed', this.url);
|
||||
@@ -217,9 +203,7 @@ export default class WebSocketClient {
|
||||
this.connectionTimeout = setTimeout(
|
||||
() => {
|
||||
if (this.stop) {
|
||||
if (this.connectionTimeout) {
|
||||
clearTimeout(this.connectionTimeout);
|
||||
}
|
||||
clearTimeout(this.connectionTimeout);
|
||||
return;
|
||||
}
|
||||
this.initialize(opts);
|
||||
@@ -230,7 +214,6 @@ export default class WebSocketClient {
|
||||
|
||||
this.conn!.onError((evt: any) => {
|
||||
if (evt.url === this.url) {
|
||||
this.hasReliablyReconnect = false;
|
||||
if (this.connectFailCount <= 1) {
|
||||
logError('websocket error', this.url);
|
||||
logError('WEBSOCKET ERROR EVENT', evt);
|
||||
@@ -260,17 +243,11 @@ export default class WebSocketClient {
|
||||
|
||||
// If we already have a connectionId present, and server sends a different one,
|
||||
// that means it's either a long timeout, or server restart, or sequence number is not found.
|
||||
// If the server is not available the first time we try to connect, we won't have a connection id
|
||||
// but still we need to sync.
|
||||
// Then we do the sync calls, and reset sequence number to 0.
|
||||
if (
|
||||
(this.connectionId !== '' && this.connectionId !== msg.data.connection_id) ||
|
||||
(this.hasReliablyReconnect && this.connectionId === '')
|
||||
) {
|
||||
logInfo(this.url, 'long timeout, or server restart, or sequence number is not found, or first connect after failure.');
|
||||
if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) {
|
||||
logInfo(this.url, 'long timeout, or server restart, or sequence number is not found.');
|
||||
this.reconnectCallback();
|
||||
this.serverSequence = 0;
|
||||
this.hasReliablyReconnect = false;
|
||||
}
|
||||
|
||||
// If it's a fresh connection, we have to set the connectionId regardless.
|
||||
@@ -338,7 +315,6 @@ export default class WebSocketClient {
|
||||
this.stop = stop;
|
||||
this.connectFailCount = 0;
|
||||
this.responseSequence = 1;
|
||||
this.hasReliablyReconnect = false;
|
||||
|
||||
if (this.conn && this.conn.readyState === WebSocketReadyState.OPEN) {
|
||||
this.conn.close();
|
||||
|
||||
@@ -37,7 +37,6 @@ type Props = {
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
zIndex: 1,
|
||||
},
|
||||
bannerContainer: {
|
||||
flex: 1,
|
||||
|
||||
88
app/components/block/index.tsx
Normal file
88
app/components/block/index.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {MessageDescriptor} from '@formatjs/intl/src/types';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginBottom: 30,
|
||||
},
|
||||
header: {
|
||||
marginHorizontal: 15,
|
||||
marginBottom: 10,
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
footer: {
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15,
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export type SectionText = {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
values?: MessageDescriptor;
|
||||
}
|
||||
|
||||
export type BlockProps = {
|
||||
children: React.ReactNode;
|
||||
disableFooter?: boolean;
|
||||
disableHeader?: boolean;
|
||||
footerText?: SectionText;
|
||||
headerText?: SectionText;
|
||||
containerStyles?: StyleProp<ViewStyle>;
|
||||
headerStyles?: StyleProp<TextStyle>;
|
||||
footerStyles?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
const Block = ({
|
||||
children,
|
||||
containerStyles,
|
||||
disableFooter,
|
||||
disableHeader,
|
||||
footerText,
|
||||
headerStyles,
|
||||
headerText,
|
||||
footerStyles,
|
||||
}: BlockProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{(headerText && !disableHeader) &&
|
||||
<FormattedText
|
||||
defaultMessage={headerText.defaultMessage}
|
||||
id={headerText.id}
|
||||
values={headerText.values}
|
||||
style={[styles.header, headerStyles]}
|
||||
/>
|
||||
}
|
||||
<View style={containerStyles}>
|
||||
{children}
|
||||
</View>
|
||||
{(footerText && !disableFooter) &&
|
||||
<FormattedText
|
||||
defaultMessage={footerText.defaultMessage}
|
||||
id={footerText.id}
|
||||
style={[styles.footer, footerStyles]}
|
||||
values={footerText.values}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Block;
|
||||
@@ -2,15 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useMemo} from 'react';
|
||||
import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
import RNButton from 'react-native-button';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
|
||||
type ConditionalProps = | {iconName: string; iconSize: number} | {iconName?: never; iconSize?: never}
|
||||
|
||||
type Props = ConditionalProps & {
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
backgroundStyle?: StyleProp<ViewStyle>;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
@@ -23,11 +20,6 @@ type Props = ConditionalProps & {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {flexDirection: 'row'},
|
||||
icon: {marginRight: 7},
|
||||
});
|
||||
|
||||
const Button = ({
|
||||
theme,
|
||||
backgroundStyle,
|
||||
@@ -39,8 +31,6 @@ const Button = ({
|
||||
onPress,
|
||||
text,
|
||||
testID,
|
||||
iconName,
|
||||
iconSize,
|
||||
}: Props) => {
|
||||
const bgStyle = useMemo(() => [
|
||||
buttonBackgroundStyle(theme, size, emphasis, buttonType, buttonState),
|
||||
@@ -58,22 +48,12 @@ const Button = ({
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
{Boolean(iconName) &&
|
||||
<CompassIcon
|
||||
name={iconName!}
|
||||
size={iconSize}
|
||||
color={StyleSheet.flatten(txtStyle).color}
|
||||
style={styles.icon}
|
||||
/>
|
||||
}
|
||||
<Text
|
||||
style={txtStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
style={txtStyle}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</RNButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {observeIsMutedSetting, observeMyChannel, queryChannelMembers} 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';
|
||||
@@ -19,6 +19,7 @@ import ChannelItem from './channel_item';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
|
||||
type EnhanceProps = WithDatabaseArgs & {
|
||||
channel: ChannelModel;
|
||||
@@ -26,6 +27,8 @@ type EnhanceProps = WithDatabaseArgs & {
|
||||
serverUrl?: string;
|
||||
}
|
||||
|
||||
const observeIsMutedSetting = (mc: MyChannelModel) => observeChannelSettings(mc.database, mc.id).pipe(switchMap((s) => of$(s?.notifyProps?.mark_unread === General.MENTION)));
|
||||
|
||||
const enhance = withObservables(['channel', 'showTeamName'], ({
|
||||
channel,
|
||||
database,
|
||||
@@ -50,7 +53,7 @@ const enhance = withObservables(['channel', 'showTeamName'], ({
|
||||
if (!mc) {
|
||||
return of$(false);
|
||||
}
|
||||
return observeIsMutedSetting(database, mc.id);
|
||||
return observeIsMutedSetting(mc);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ const getStyle = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
zIndex: 1,
|
||||
},
|
||||
bannerContainerNotConnected: {
|
||||
...bannerContainer,
|
||||
|
||||
@@ -175,12 +175,10 @@ const OptionItem = ({
|
||||
/>
|
||||
);
|
||||
} else if (type === OptionType.RADIO) {
|
||||
const radioComponentTestId = selected ? `${testID}.selected` : `${testID}.not_selected`;
|
||||
radioComponent = (
|
||||
<RadioItem
|
||||
selected={Boolean(selected)}
|
||||
{...radioItemProps}
|
||||
testID={radioComponentTestId}
|
||||
/>
|
||||
);
|
||||
} else if (type === OptionType.TOGGLE) {
|
||||
|
||||
@@ -38,9 +38,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
export type RadioItemProps = {
|
||||
selected: boolean;
|
||||
checkedBody?: boolean;
|
||||
testID?: string;
|
||||
}
|
||||
const RadioItem = ({selected, checkedBody, testID}: RadioItemProps) => {
|
||||
const RadioItem = ({selected, checkedBody}: RadioItemProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
@@ -61,10 +60,7 @@ const RadioItem = ({selected, checkedBody, testID}: RadioItemProps) => {
|
||||
}, [checkedBody]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.ring, !selected && styles.inActive]}
|
||||
testID={testID}
|
||||
>
|
||||
<View style={[styles.ring, !selected && styles.inActive]}>
|
||||
{selected && getBody()}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useInputPropagation} from '@hooks/input';
|
||||
import {t} from '@i18n';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {extractFileInfo} from '@utils/file';
|
||||
@@ -121,7 +120,6 @@ export default function PostInput({
|
||||
const style = getStyleSheet(theme);
|
||||
const serverUrl = useServerUrl();
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const [propagateValue, shouldProcessEvent] = useInputPropagation();
|
||||
|
||||
const lastTypingEventSent = useRef(0);
|
||||
|
||||
@@ -136,8 +134,12 @@ export default function PostInput({
|
||||
return {...style.input, maxHeight};
|
||||
}, [maxHeight, style.input]);
|
||||
|
||||
const blur = () => {
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleAndroidKeyboard = () => {
|
||||
onBlur();
|
||||
blur();
|
||||
};
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
@@ -182,9 +184,6 @@ export default function PostInput({
|
||||
}, [updateCursorPosition, cursorPosition]);
|
||||
|
||||
const handleTextChange = useCallback((newValue: string) => {
|
||||
if (!shouldProcessEvent(newValue)) {
|
||||
return;
|
||||
}
|
||||
updateValue(newValue);
|
||||
lastNativeValue.current = newValue;
|
||||
|
||||
@@ -229,16 +228,10 @@ export default function PostInput({
|
||||
case 'enter':
|
||||
sendMessage();
|
||||
break;
|
||||
case 'shift-enter': {
|
||||
let newValue: string;
|
||||
updateValue((v) => {
|
||||
newValue = v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition);
|
||||
return newValue;
|
||||
});
|
||||
case 'shift-enter':
|
||||
updateValue((v) => v.substring(0, cursorPosition) + '\n' + v.substring(cursorPosition));
|
||||
updateCursorPosition((pos) => pos + 1);
|
||||
propagateValue(newValue!);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [sendMessage, updateValue, cursorPosition, isTablet]);
|
||||
@@ -277,19 +270,18 @@ export default function PostInput({
|
||||
const draft = value ? `${value} ${text} ` : `${text} `;
|
||||
updateValue(draft);
|
||||
updateCursorPosition(draft.length);
|
||||
propagateValue(draft);
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
listener.remove();
|
||||
updateDraftMessage(serverUrl, channelId, rootId, lastNativeValue.current); // safe draft on unmount
|
||||
};
|
||||
}, [updateValue, channelId, rootId]);
|
||||
return () => listener.remove();
|
||||
}, [updateValue, value, channelId, rootId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== lastNativeValue.current) {
|
||||
propagateValue(value);
|
||||
// May change when we implement Fabric
|
||||
inputRef.current?.setNativeProps({
|
||||
text: value,
|
||||
});
|
||||
lastNativeValue.current = value;
|
||||
}
|
||||
}, [value]);
|
||||
@@ -322,7 +314,6 @@ export default function PostInput({
|
||||
testID={testID}
|
||||
underlineColorAndroid='transparent'
|
||||
textContentType='none'
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ const CombinedUserActivity = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const passProps = {post, sourceScreen: location};
|
||||
const passProps = {post};
|
||||
Keyboard.dismiss();
|
||||
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
|
||||
|
||||
@@ -117,7 +117,7 @@ const CombinedUserActivity = ({
|
||||
} else {
|
||||
showModalOverCurrentContext(Screens.POST_OPTIONS, passProps, bottomSheetModalOptions(theme));
|
||||
}
|
||||
}, [post, canDelete, isTablet, intl, location]);
|
||||
}, [post, canDelete, isTablet, intl]);
|
||||
|
||||
const renderMessage = (postType: string, userIds: string[], actorId: string) => {
|
||||
let actor = '';
|
||||
|
||||
@@ -19,25 +19,21 @@ import PostList from './post_list';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const enhancedWithoutPosts = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables(['posts'], ({database, posts}: {posts: PostModel[]} & WithDatabaseArgs) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const postIds = posts.map((p) => p.id);
|
||||
|
||||
return {
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone || null))))),
|
||||
currentUserId: currentUser.pipe((switchMap((user) => of$(user?.id)))),
|
||||
currentUsername: currentUser.pipe((switchMap((user) => of$(user?.username)))),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observeWithColumns(['name']).pipe(
|
||||
savedPostIds: observeSavedPostsByIds(database, postIds),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const enhanced = withObservables(['posts'], ({database, posts}: {posts: PostModel[]} & WithDatabaseArgs) => {
|
||||
const postIds = posts.map((p) => p.id);
|
||||
return {
|
||||
savedPostIds: observeSavedPostsByIds(database, postIds),
|
||||
};
|
||||
});
|
||||
|
||||
export default React.memo(withDatabase(enhancedWithoutPosts(enhanced(PostList))));
|
||||
export default React.memo(withDatabase(enhanced(PostList)));
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {of as of$, first as first$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeMyChannel} from '@queries/servers/channel';
|
||||
import {observeThreadById} from '@queries/servers/thread';
|
||||
@@ -32,14 +31,8 @@ const enhanced = withObservables(['channelId', 'isCRTEnabled', 'rootId'], ({chan
|
||||
}
|
||||
|
||||
const myChannel = observeMyChannel(database, channelId);
|
||||
const isManualUnread = myChannel.pipe(
|
||||
switchMap((ch) => of$(ch?.manuallyUnread)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const unreadCount = myChannel.pipe(
|
||||
switchMap((ch) => of$(ch?.messageCount)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
const isManualUnread = myChannel.pipe(switchMap((ch) => of$(ch?.manuallyUnread)));
|
||||
const unreadCount = myChannel.pipe(switchMap((ch) => of$(ch?.messageCount)));
|
||||
|
||||
return {
|
||||
isManualUnread,
|
||||
@@ -47,4 +40,4 @@ const enhanced = withObservables(['channelId', 'isCRTEnabled', 'rootId'], ({chan
|
||||
};
|
||||
});
|
||||
|
||||
export default React.memo(withDatabase(enhanced(MoreMessages)));
|
||||
export default withDatabase(enhanced(MoreMessages));
|
||||
|
||||
@@ -86,11 +86,11 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
if (reaction) {
|
||||
const emojiAlias = getEmojiFirstAlias(reaction.emojiName);
|
||||
if (acc.has(emojiAlias)) {
|
||||
const rs = acc.get(emojiAlias)!;
|
||||
const rs = acc.get(emojiAlias);
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const present = rs.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1;
|
||||
if (!present) {
|
||||
rs.push(reaction);
|
||||
rs!.push(reaction);
|
||||
}
|
||||
} else {
|
||||
acc.set(emojiAlias, [reaction]);
|
||||
@@ -105,7 +105,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
}, new Map<string, ReactionModel[]>());
|
||||
|
||||
return {reactionsByName, highlightedReactions};
|
||||
}, [sortedReactions, reactions]);
|
||||
}, [sortedReactions]);
|
||||
|
||||
const handleAddReactionToPost = (emoji: string) => {
|
||||
addReaction(serverUrl, postId, emoji);
|
||||
@@ -178,7 +178,7 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled,
|
||||
return (
|
||||
<Reaction
|
||||
key={r}
|
||||
count={reaction?.length || 1}
|
||||
count={reaction!.length}
|
||||
emojiName={r}
|
||||
highlight={highlightedReactions.includes(r)}
|
||||
onPress={handleReactionPress}
|
||||
|
||||
@@ -150,7 +150,6 @@ export default function SelectedUsers({
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const numberSelectedIds = Object.keys(selectedIds).length;
|
||||
const bottomSpace = (dimensions.height - containerHeight - modalPosition);
|
||||
const bottomPaddingBottom = isTablet ? CHIP_HEIGHT_WITH_MARGIN : 0;
|
||||
|
||||
const users = useMemo(() => {
|
||||
const u = [];
|
||||
@@ -173,8 +172,8 @@ export default function SelectedUsers({
|
||||
}, [selectedIds, teammateNameDisplay, onRemove]);
|
||||
|
||||
const totalPanelHeight = useDerivedValue(() => (
|
||||
isVisible ? panelHeight.value + BUTTON_HEIGHT + bottomPaddingBottom : 0
|
||||
), [isVisible, isTablet, bottomPaddingBottom]);
|
||||
isVisible ? panelHeight.value + BUTTON_HEIGHT : 0
|
||||
), [isVisible, isTablet]);
|
||||
|
||||
const marginBottom = useMemo(() => {
|
||||
let margin = keyboard.height && Platform.OS === 'ios' ? keyboard.height - insets.bottom : 0;
|
||||
@@ -209,7 +208,7 @@ export default function SelectedUsers({
|
||||
}, [onPress]);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
panelHeight.value = Math.min(PANEL_MAX_HEIGHT + bottomPaddingBottom, e.nativeEvent.layout.height);
|
||||
panelHeight.value = Math.min(PANEL_MAX_HEIGHT, e.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
const androidMaxHeight = Platform.select({
|
||||
@@ -236,8 +235,8 @@ export default function SelectedUsers({
|
||||
const animatedViewStyle = useAnimatedStyle(() => ({
|
||||
height: withTiming(totalPanelHeight.value + insets.bottom, {duration: 250}),
|
||||
borderWidth: isVisible ? 1 : 0,
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + bottomPaddingBottom + insets.bottom : 0,
|
||||
}), [isVisible, insets, bottomPaddingBottom]);
|
||||
maxHeight: isVisible ? PANEL_MAX_HEIGHT + BUTTON_HEIGHT + insets.bottom : 0,
|
||||
}), [isVisible, insets]);
|
||||
|
||||
const animatedButtonStyle = useAnimatedStyle(() => ({
|
||||
opacity: withTiming(isVisible ? 1 : 0, {duration: isVisible ? 500 : 100}),
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
|
||||
type SectionText = {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
values?: MessageDescriptor;
|
||||
}
|
||||
|
||||
type SettingBlockProps = {
|
||||
children: React.ReactNode;
|
||||
containerStyles?: StyleProp<ViewStyle>;
|
||||
disableFooter?: boolean;
|
||||
disableHeader?: boolean;
|
||||
footerStyles?: StyleProp<TextStyle>;
|
||||
footerText?: SectionText;
|
||||
headerStyles?: StyleProp<TextStyle>;
|
||||
headerText?: SectionText;
|
||||
onLayout?: (event: LayoutChangeEvent) => void;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
marginBottom: 30,
|
||||
},
|
||||
contentContainerStyle: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
header: {
|
||||
color: theme.centerChannelColor,
|
||||
...typography('Heading', 300, 'SemiBold'),
|
||||
marginBottom: 8,
|
||||
marginLeft: 20,
|
||||
marginTop: 12,
|
||||
marginRight: 15,
|
||||
},
|
||||
footer: {
|
||||
marginTop: 10,
|
||||
marginHorizontal: 15,
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const SettingBlock = ({
|
||||
children, containerStyles, disableFooter, disableHeader,
|
||||
footerStyles, footerText, headerStyles, headerText, onLayout,
|
||||
}: SettingBlockProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
{(headerText && !disableHeader) &&
|
||||
<FormattedText
|
||||
defaultMessage={headerText.defaultMessage}
|
||||
id={headerText.id}
|
||||
values={headerText.values}
|
||||
style={[styles.header, headerStyles]}
|
||||
/>
|
||||
}
|
||||
<View style={[styles.contentContainerStyle, containerStyles]}>
|
||||
{children}
|
||||
</View>
|
||||
{(footerText && !disableFooter) &&
|
||||
<FormattedText
|
||||
defaultMessage={footerText.defaultMessage}
|
||||
id={footerText.id}
|
||||
style={[styles.footer, footerStyles]}
|
||||
values={footerText.values}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingBlock;
|
||||
@@ -14,7 +14,7 @@ import TeamList from './team_list';
|
||||
type Props = {
|
||||
iconPad?: boolean;
|
||||
canJoinOtherTeams: boolean;
|
||||
hasMoreThanOneTeam: boolean;
|
||||
teamsCount: number;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -36,8 +36,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneTeam}: Props) {
|
||||
const initialWidth = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
export default function TeamSidebar({iconPad, canJoinOtherTeams, teamsCount}: Props) {
|
||||
const initialWidth = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
const width = useSharedValue(initialWidth);
|
||||
const marginTop = useSharedValue(iconPad ? 44 : 0);
|
||||
const theme = useTheme();
|
||||
@@ -58,8 +58,8 @@ export default function TeamSidebar({iconPad, canJoinOtherTeams, hasMoreThanOneT
|
||||
}, [iconPad]);
|
||||
|
||||
useEffect(() => {
|
||||
width.value = hasMoreThanOneTeam ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
}, [hasMoreThanOneTeam]);
|
||||
width.value = teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0;
|
||||
}, [teamsCount]);
|
||||
|
||||
return (
|
||||
<Animated.View style={[styles.container, transform]}>
|
||||
|
||||
@@ -25,13 +25,19 @@ const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
const TouchableEmoji = ({category, name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const onPress = useCallback(preventDoubleTap(() => onEmojiPress(name)), []);
|
||||
|
||||
let emoji;
|
||||
if (category && CATEGORIES_WITH_SKINS.includes(category)) {
|
||||
return (
|
||||
emoji = (
|
||||
<SkinnedEmoji
|
||||
name={name}
|
||||
onEmojiPress={onEmojiPress}
|
||||
size={size}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
emoji = (
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -46,10 +52,7 @@ const TouchableEmoji = ({category, name, onEmojiPress, size = 30, style}: Props)
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={size}
|
||||
/>
|
||||
{emoji}
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {StyleProp, View, ViewStyle} from 'react-native';
|
||||
import React, {useMemo} from 'react';
|
||||
|
||||
import {useEmojiSkinTone} from '@app/hooks/emoji_category_bar';
|
||||
import Emoji from '@components/emoji';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {useEmojiSkinTone} from '@hooks/emoji_category_bar';
|
||||
import {skinCodes} from '@utils/emoji';
|
||||
import {isValidNamedEmoji} from '@utils/emoji/helpers';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
onEmojiPress: (emoji: string) => void;
|
||||
size?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const hitSlop = {top: 10, bottom: 10, left: 10, right: 10};
|
||||
|
||||
const SkinnedEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
|
||||
const SkinnedEmoji = ({name, size = 30}: Props) => {
|
||||
const skinTone = useEmojiSkinTone();
|
||||
const emojiName = useMemo(() => {
|
||||
const skinnedEmoji = `${name}_${skinCodes[skinTone]}`;
|
||||
@@ -30,26 +23,11 @@ const SkinnedEmoji = ({name, onEmojiPress, size = 30, style}: Props) => {
|
||||
return skinnedEmoji;
|
||||
}, [name, skinTone]);
|
||||
|
||||
const onPress = useCallback(preventDoubleTap(() => {
|
||||
onEmojiPress(emojiName);
|
||||
}), [emojiName]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={style}
|
||||
>
|
||||
<TouchableWithFeedback
|
||||
hitSlop={hitSlop}
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={size}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
</View>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
export const CATEGORIES_TO_KEEP: Record<string, string> = {
|
||||
ADVANCED_SETTINGS: 'advanced_settings',
|
||||
CHANNEL_APPROXIMATE_VIEW_TIME: 'channel_approximate_view_time',
|
||||
CHANNEL_OPEN_TIME: 'channel_open_time',
|
||||
DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
GROUP_CHANNEL_SHOW: 'group_channel_show',
|
||||
DISPLAY_SETTINGS: 'display_settings',
|
||||
|
||||
@@ -10,7 +10,7 @@ export const CALL = 'Call';
|
||||
export const CHANNEL = 'Channel';
|
||||
export const CHANNEL_ADD_PEOPLE = 'ChannelAddPeople';
|
||||
export const CHANNEL_INFO = 'ChannelInfo';
|
||||
export const CHANNEL_NOTIFICATION_PREFERENCES = 'ChannelNotificationPreferences';
|
||||
export const CHANNEL_MENTION = 'ChannelMention';
|
||||
export const CODE = 'Code';
|
||||
export const CREATE_DIRECT_MESSAGE = 'CreateDirectMessage';
|
||||
export const CREATE_OR_EDIT_CHANNEL = 'CreateOrEditChannel';
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
CHANNEL,
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_INFO,
|
||||
CHANNEL_NOTIFICATION_PREFERENCES,
|
||||
CHANNEL_MENTION,
|
||||
CODE,
|
||||
CREATE_DIRECT_MESSAGE,
|
||||
CREATE_OR_EDIT_CHANNEL,
|
||||
@@ -172,5 +172,6 @@ export const SCREENS_AS_BOTTOM_SHEET = new Set<string>([
|
||||
|
||||
export const NOT_READY = [
|
||||
CHANNEL_ADD_PEOPLE,
|
||||
CHANNEL_MENTION,
|
||||
CREATE_TEAM,
|
||||
];
|
||||
|
||||
@@ -44,8 +44,6 @@ const WebsocketEvents = {
|
||||
EMOJI_ADDED: 'emoji_added',
|
||||
LICENSE_CHANGED: 'license_changed',
|
||||
CONFIG_CHANGED: 'config_changed',
|
||||
PLUGIN_ENABLED: 'plugin_enabled',
|
||||
PLUGIN_DISABLED: 'plugin_disabled',
|
||||
PLUGIN_STATUSES_CHANGED: 'plugin_statuses_changed',
|
||||
OPEN_DIALOG: 'open_dialog',
|
||||
INCREASE_POST_VISIBILITY_BY_ONE: 'increase_post_visibility_by_one',
|
||||
|
||||
@@ -246,11 +246,10 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
||||
const totalMsg = isCRT ? channel.total_msg_count_root! : channel.total_msg_count;
|
||||
const myMsgCount = isCRT ? my.msg_count_root! : my.msg_count;
|
||||
const msgCount = Math.max(0, totalMsg - myMsgCount);
|
||||
const lastPostAt = isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at;
|
||||
my.msg_count = msgCount;
|
||||
my.mention_count = isCRT ? my.mention_count_root! : my.mention_count;
|
||||
my.is_unread = msgCount > 0;
|
||||
my.last_post_at = lastPostAt;
|
||||
my.last_post_at = (isCRT ? (channel.last_root_post_at || channel.last_post_at) : channel.last_post_at) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +271,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
||||
}
|
||||
|
||||
const chan = channelMap[my.channel_id];
|
||||
const lastPostAt = isCRT ? (chan.last_root_post_at || chan.last_post_at) : chan.last_post_at;
|
||||
const lastPostAt = (isCRT ? chan.last_root_post_at : chan.last_post_at) || 0;
|
||||
if ((chan && e.lastPostAt < lastPostAt) ||
|
||||
e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at ||
|
||||
e.roles !== my.roles
|
||||
@@ -319,12 +318,12 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
||||
}));
|
||||
|
||||
const uniqueRaws = getUniqueRawsBy({raws: memberships, key: 'id'}) as ChannelMember[];
|
||||
const ids = uniqueRaws.map((cm: ChannelMember) => `${cm.channel_id}-${cm.user_id}`);
|
||||
const ids = uniqueRaws.map((cm: ChannelMember) => cm.channel_id);
|
||||
const db: Database = this.database;
|
||||
const existing = await db.get<ChannelMembershipModel>(CHANNEL_MEMBERSHIP).query(
|
||||
Q.where('id', Q.oneOf(ids)),
|
||||
).fetch();
|
||||
const membershipMap = new Map<string, ChannelMembershipModel>(existing.map((member) => [member.channelId, member]));
|
||||
const membershipMap = new Map<string, ChannelMembershipModel>(existing.map((member) => [member.id, member]));
|
||||
const createOrUpdateRawValues = uniqueRaws.reduce((res: ChannelMember[], cm) => {
|
||||
const e = membershipMap.get(cm.channel_id);
|
||||
if (!e) {
|
||||
|
||||
@@ -148,15 +148,13 @@ const GroupHandler = <TBase extends Constructor<ServerDataOperatorBase>>(supercl
|
||||
rawValues.push(...Object.values(groupsSet));
|
||||
}
|
||||
|
||||
if (rawValues.length) {
|
||||
records.push(...(await this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformGroupMembershipRecord,
|
||||
createOrUpdateRawValues: rawValues,
|
||||
tableName: GROUP_MEMBERSHIP,
|
||||
prepareRecordsOnly: true,
|
||||
}, 'handleGroupMembershipsForMember')));
|
||||
}
|
||||
records.push(...(await this.handleRecords({
|
||||
fieldName: 'id',
|
||||
transformer: transformGroupMembershipRecord,
|
||||
createOrUpdateRawValues: rawValues,
|
||||
tableName: GROUP_MEMBERSHIP,
|
||||
prepareRecordsOnly: true,
|
||||
}, 'handleGroupMembershipsForMember')));
|
||||
|
||||
// Batch update if there are records
|
||||
if (records.length && !prepareRecordsOnly) {
|
||||
|
||||
@@ -164,11 +164,11 @@ describe('*** Operator: Thread Handlers tests ***', () => {
|
||||
|
||||
expect(spyOnPrepareRecords).toHaveBeenCalledWith({
|
||||
createRaws: [{
|
||||
raw: {team_id: 'team_id_1', thread_id: 'thread-1'},
|
||||
}, {
|
||||
raw: {team_id: 'team_id_1', thread_id: 'thread-2'},
|
||||
record: undefined,
|
||||
}, {
|
||||
raw: {team_id: 'team_id_2', thread_id: 'thread-2'},
|
||||
record: undefined,
|
||||
}],
|
||||
transformer: transformThreadInTeamRecord,
|
||||
tableName: 'ThreadsInTeam',
|
||||
|
||||
@@ -124,9 +124,7 @@ const ThreadHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superc
|
||||
threadsMap: {[teamId]: threads},
|
||||
prepareRecordsOnly: true,
|
||||
}) as ThreadInTeamModel[];
|
||||
if (threadsInTeam.length) {
|
||||
batch.push(...threadsInTeam);
|
||||
}
|
||||
batch.push(...threadsInTeam);
|
||||
}
|
||||
|
||||
if (batch.length && !prepareRecordsOnly) {
|
||||
@@ -201,7 +199,7 @@ const ThreadHandler = <TBase extends Constructor<ServerDataOperatorBase>>(superc
|
||||
const threadIds = threadsMap[teamId].map((thread) => thread.id);
|
||||
const chunks = await (this.database as Database).get<ThreadInTeamModel>(THREADS_IN_TEAM).query(
|
||||
Q.where('team_id', teamId),
|
||||
Q.where('thread_id', Q.oneOf(threadIds)),
|
||||
Q.where('id', Q.oneOf(threadIds)),
|
||||
).fetch();
|
||||
const chunksMap = chunks.reduce((result: Record<string, ThreadInTeamModel>, chunk) => {
|
||||
result[chunk.threadId] = chunk;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {sanitizeLikeString} from '.';
|
||||
|
||||
describe('Test SQLite Sanitize like string with latin and non-latin characters', () => {
|
||||
const disallowed = ',./;[]!@#$%^&*()_-=+~';
|
||||
|
||||
test('test (latin)', () => {
|
||||
expect(sanitizeLikeString('test123')).toBe('test123');
|
||||
expect(sanitizeLikeString(`test123${disallowed}`)).toBe(`test123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (arabic)', () => {
|
||||
expect(sanitizeLikeString('اختبار123')).toBe('اختبار123');
|
||||
expect(sanitizeLikeString(`اختبار123${disallowed}`)).toBe(`اختبار123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (greek)', () => {
|
||||
expect(sanitizeLikeString('δοκιμή123')).toBe('δοκιμή123');
|
||||
expect(sanitizeLikeString(`δοκιμή123${disallowed}`)).toBe(`δοκιμή123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (hebrew)', () => {
|
||||
expect(sanitizeLikeString('חשבון123')).toBe('חשבון123');
|
||||
expect(sanitizeLikeString(`חשבון123${disallowed}`)).toBe(`חשבון123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (russian)', () => {
|
||||
expect(sanitizeLikeString('тест123')).toBe('тест123');
|
||||
expect(sanitizeLikeString(`тест123${disallowed}`)).toBe(`тест123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (chinese trad)', () => {
|
||||
expect(sanitizeLikeString('測試123')).toBe('測試123');
|
||||
expect(sanitizeLikeString(`測試123${disallowed}`)).toBe(`測試123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
|
||||
test('test (japanese)', () => {
|
||||
expect(sanitizeLikeString('テスト123')).toBe('テスト123');
|
||||
expect(sanitizeLikeString(`テスト123${disallowed}`)).toBe(`テスト123${'_'.repeat(disallowed.length)}`);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import xRegExp from 'xregexp';
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
import type Model from '@nozbe/watermelondb/Model';
|
||||
@@ -93,7 +91,3 @@ export const filterAndSortMyChannels = ([myChannels, channels, notifyProps]: Fil
|
||||
|
||||
return [...mentions, ...unreads, ...mutedMentions];
|
||||
};
|
||||
|
||||
// Matches letters from any alphabet and numbers
|
||||
const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g');
|
||||
export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_');
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {useCallback, useRef} from 'react';
|
||||
import {Platform} from 'react-native';
|
||||
|
||||
export function useInputPropagation(): [(v: string) => void, (v: string) => boolean] {
|
||||
const waitForValue = useRef<string>();
|
||||
const waitToPropagate = useCallback((value: string) => {
|
||||
waitForValue.current = value;
|
||||
}, []);
|
||||
const shouldProcessEvent = useCallback((newValue: string) => {
|
||||
if (Platform.OS === 'android') {
|
||||
return true;
|
||||
}
|
||||
if (waitForValue.current === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (newValue === waitForValue.current) {
|
||||
waitForValue.current = undefined;
|
||||
}
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
return [waitToPropagate, shouldProcessEvent];
|
||||
}
|
||||
@@ -52,6 +52,6 @@ export async function start() {
|
||||
|
||||
registerNavigationListeners();
|
||||
registerScreens();
|
||||
await WebsocketManager.init(serverCredentials);
|
||||
initialLaunch();
|
||||
await initialLaunch();
|
||||
WebsocketManager.init(serverCredentials);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, Pu
|
||||
export const initialLaunch = async () => {
|
||||
const deepLinkUrl = await Linking.getInitialURL();
|
||||
if (deepLinkUrl) {
|
||||
return launchAppFromDeepLink(deepLinkUrl, true);
|
||||
await launchAppFromDeepLink(deepLinkUrl, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await Notifications.getInitialNotification();
|
||||
@@ -42,10 +43,11 @@ export const initialLaunch = async () => {
|
||||
tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null;
|
||||
}
|
||||
if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) {
|
||||
return launchAppFromNotification(convertToNotificationData(notification!), true);
|
||||
await launchAppFromNotification(convertToNotificationData(notification!), true);
|
||||
return;
|
||||
}
|
||||
|
||||
return launchApp({launchType: Launch.Normal, coldStart: notification ? tapped : true});
|
||||
await launchApp({launchType: Launch.Normal, coldStart: true});
|
||||
};
|
||||
|
||||
const launchAppFromDeepLink = async (deepLinkUrl: string, coldStart = false) => {
|
||||
@@ -160,17 +162,14 @@ const launchToHome = async (props: LaunchProps) => {
|
||||
const extra = props.extra as NotificationWithData;
|
||||
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
|
||||
if (openPushNotification) {
|
||||
await resetToHome(props);
|
||||
return pushNotificationEntry(props.serverUrl!, extra.payload!);
|
||||
pushNotificationEntry(props.serverUrl!, extra);
|
||||
} else {
|
||||
appEntry(props.serverUrl!);
|
||||
}
|
||||
|
||||
appEntry(props.serverUrl!);
|
||||
break;
|
||||
}
|
||||
case Launch.Normal:
|
||||
if (props.coldStart) {
|
||||
appEntry(props.serverUrl!);
|
||||
}
|
||||
appEntry(props.serverUrl!);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -203,21 +202,16 @@ export const getLaunchPropsFromNotification = async (notification: NotificationW
|
||||
|
||||
const {payload} = notification;
|
||||
launchProps.extra = notification;
|
||||
let serverUrl: string | undefined;
|
||||
|
||||
try {
|
||||
if (payload?.server_url) {
|
||||
DatabaseManager.getServerDatabaseAndOperator(payload.server_url);
|
||||
serverUrl = payload.server_url;
|
||||
} else if (payload?.server_id) {
|
||||
serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
|
||||
if (payload?.server_url) {
|
||||
launchProps.serverUrl = payload.server_url;
|
||||
} else if (payload?.server_id) {
|
||||
const serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id);
|
||||
if (serverUrl) {
|
||||
launchProps.serverUrl = serverUrl;
|
||||
} else {
|
||||
launchProps.launchError = true;
|
||||
}
|
||||
} catch {
|
||||
launchProps.launchError = true;
|
||||
}
|
||||
|
||||
if (serverUrl) {
|
||||
launchProps.serverUrl = serverUrl;
|
||||
} else {
|
||||
launchProps.launchError = true;
|
||||
}
|
||||
|
||||
@@ -37,22 +37,22 @@ class PushNotifications {
|
||||
configured = false;
|
||||
|
||||
init(register: boolean) {
|
||||
if (register) {
|
||||
this.registerIfNeeded();
|
||||
}
|
||||
|
||||
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
|
||||
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
|
||||
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
|
||||
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
|
||||
|
||||
if (register) {
|
||||
this.registerIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
async registerIfNeeded() {
|
||||
const isRegistered = await Notifications.isRegisteredForRemoteNotifications();
|
||||
if (!isRegistered) {
|
||||
await requestNotifications(['alert', 'sound', 'badge']);
|
||||
Notifications.registerRemoteNotifications();
|
||||
}
|
||||
Notifications.registerRemoteNotifications();
|
||||
}
|
||||
|
||||
createReplyCategory = () => {
|
||||
|
||||
@@ -33,7 +33,6 @@ class WebsocketManager {
|
||||
private previousActiveState: boolean;
|
||||
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
|
||||
private backgroundIntervalId: number | undefined;
|
||||
private firstConnectionSynced: Record<string, boolean> = {};
|
||||
|
||||
constructor() {
|
||||
this.previousActiveState = AppState.currentState === 'active';
|
||||
@@ -41,15 +40,21 @@ class WebsocketManager {
|
||||
|
||||
public init = async (serverCredentials: ServerCredential[]) => {
|
||||
this.netConnected = Boolean((await NetInfo.fetch()).isConnected);
|
||||
serverCredentials.forEach(
|
||||
({serverUrl, token}) => {
|
||||
try {
|
||||
DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
this.createClient(serverUrl, token, 0);
|
||||
} catch (error) {
|
||||
logError('WebsocketManager init error', error);
|
||||
}
|
||||
},
|
||||
await Promise.all(
|
||||
serverCredentials.map(
|
||||
async ({serverUrl, token}) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.createClient(serverUrl, token, 0);
|
||||
} catch (error) {
|
||||
logError('WebsocketManager init error', error);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange);
|
||||
@@ -63,7 +68,6 @@ class WebsocketManager {
|
||||
this.connectionTimerIDs[serverUrl].cancel();
|
||||
}
|
||||
delete this.clients[serverUrl];
|
||||
delete this.firstConnectionSynced[serverUrl];
|
||||
|
||||
this.getConnectedSubject(serverUrl).next('not_connected');
|
||||
delete this.connectedSubjects[serverUrl];
|
||||
@@ -80,6 +84,9 @@ class WebsocketManager {
|
||||
client.setReliableReconnectCallback(() => this.onReliableReconnect(serverUrl));
|
||||
client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect));
|
||||
|
||||
if (this.netConnected && ['unknown', 'active'].includes(AppState.currentState)) {
|
||||
client.initialize();
|
||||
}
|
||||
this.clients[serverUrl] = client;
|
||||
|
||||
return this.clients[serverUrl];
|
||||
@@ -136,34 +143,25 @@ class WebsocketManager {
|
||||
}
|
||||
};
|
||||
|
||||
public initializeClient = async (serverUrl: string) => {
|
||||
private initializeClient = (serverUrl: string) => {
|
||||
const client: WebSocketClient = this.clients[serverUrl];
|
||||
this.connectionTimerIDs[serverUrl]?.cancel();
|
||||
delete this.connectionTimerIDs[serverUrl];
|
||||
if (!client?.isConnected()) {
|
||||
client.initialize();
|
||||
if (!this.firstConnectionSynced[serverUrl]) {
|
||||
const error = await handleFirstConnect(serverUrl);
|
||||
if (error) {
|
||||
client.close(false);
|
||||
}
|
||||
this.firstConnectionSynced[serverUrl] = true;
|
||||
}
|
||||
}
|
||||
this.connectionTimerIDs[serverUrl]?.cancel();
|
||||
delete this.connectionTimerIDs[serverUrl];
|
||||
};
|
||||
|
||||
private onFirstConnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next('connected');
|
||||
handleFirstConnect(serverUrl);
|
||||
};
|
||||
|
||||
private onReconnect = async (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next('connected');
|
||||
const error = await handleReconnect(serverUrl);
|
||||
if (error) {
|
||||
this.getClient(serverUrl)?.close(false);
|
||||
}
|
||||
await handleReconnect(serverUrl);
|
||||
};
|
||||
|
||||
private onReliableReconnect = async (serverUrl: string) => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import InCallManager from 'react-native-incall-manager';
|
||||
import * as CallsActions from '@calls/actions';
|
||||
import {getConnectionForTesting} from '@calls/actions/calls';
|
||||
import * as Permissions from '@calls/actions/permissions';
|
||||
import {needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
|
||||
import * as State from '@calls/state';
|
||||
import {
|
||||
myselfLeftCall,
|
||||
@@ -64,8 +63,6 @@ const mockClient = {
|
||||
]
|
||||
)),
|
||||
enableChannelCalls: jest.fn(),
|
||||
startCallRecording: jest.fn(),
|
||||
stopCallRecording: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('@calls/connection/connection', () => ({
|
||||
@@ -77,8 +74,6 @@ jest.mock('@calls/connection/connection', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
|
||||
const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
const call = {
|
||||
participants: {
|
||||
@@ -97,7 +92,7 @@ const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
hostId: 'xohi8cki9787fgiryne716u84o',
|
||||
} as Call;
|
||||
act(() => {
|
||||
State.setCallsState(serverUrl, {myUserId: 'myUserId', calls: {}, enabled: {}});
|
||||
State.setCallsState(serverUrl, {serverUrl, myUserId: 'myUserId', calls: {}, enabled: {}});
|
||||
State.callStarted(serverUrl, call);
|
||||
});
|
||||
};
|
||||
@@ -277,6 +272,7 @@ describe('Actions.Calls', () => {
|
||||
|
||||
it('loadCalls fails from server', async () => {
|
||||
const expectedCallsState: CallsState = {
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'userId1',
|
||||
calls: {},
|
||||
enabled: {},
|
||||
@@ -343,23 +339,4 @@ describe('Actions.Calls', () => {
|
||||
expect(mockClient.enableChannelCalls).toBeCalledWith('channel-1', false);
|
||||
assert.equal(result.current.enabled['channel-1'], false);
|
||||
});
|
||||
|
||||
it('startCallRecording', async () => {
|
||||
await act(async () => {
|
||||
await CallsActions.startCallRecording('server1', 'channel-id');
|
||||
});
|
||||
|
||||
expect(mockClient.startCallRecording).toBeCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toBeCalled();
|
||||
});
|
||||
|
||||
it('stopCallRecording', async () => {
|
||||
await act(async () => {
|
||||
await CallsActions.stopCallRecording('server1', 'channel-id');
|
||||
});
|
||||
|
||||
expect(mockClient.stopCallRecording).toBeCalledWith('channel-id');
|
||||
expect(needsRecordingErrorAlert).toBeCalled();
|
||||
expect(needsRecordingWillBePostedAlert).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
|
||||
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert} from '@calls/alerts';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
@@ -411,8 +411,6 @@ export const endCall = async (serverUrl: string, channelId: string) => {
|
||||
};
|
||||
|
||||
export const startCallRecording = async (serverUrl: string, callId: string) => {
|
||||
needsRecordingErrorAlert();
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
let data: ApiResp | RecordingState;
|
||||
@@ -429,7 +427,6 @@ export const startCallRecording = async (serverUrl: string, callId: string) => {
|
||||
|
||||
export const stopCallRecording = async (serverUrl: string, callId: string) => {
|
||||
needsRecordingWillBePostedAlert();
|
||||
needsRecordingErrorAlert();
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
@@ -484,73 +481,6 @@ export const handleCallsSlashCommand = async (value: string, serverUrl: string,
|
||||
defaultMessage: 'You\'re not connected to a call in the current channel.',
|
||||
}),
|
||||
};
|
||||
case 'recording': {
|
||||
if (tokens.length < 3) {
|
||||
return {handled: false};
|
||||
}
|
||||
|
||||
const action = tokens[2];
|
||||
const currentCall = getCurrentCall();
|
||||
const recording = currentCall?.recState;
|
||||
const isHost = currentCall?.hostId === currentUserId;
|
||||
|
||||
if (currentCall?.channelId !== channelId) {
|
||||
return {
|
||||
error: intl.formatMessage({
|
||||
id: 'mobile.calls_not_connected',
|
||||
defaultMessage: 'You\'re not connected to a call in the current channel.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'start') {
|
||||
if (recording && recording.start_at > recording.end_at) {
|
||||
return {
|
||||
error: intl.formatMessage({
|
||||
id: 'mobile.calls_recording_start_in_progress',
|
||||
defaultMessage: 'A recording is already in progress.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isHost) {
|
||||
return {
|
||||
error: intl.formatMessage({
|
||||
id: 'mobile.calls_recording_start_no_permissions',
|
||||
defaultMessage: 'You don\'t have permissions to start a recording. Please ask the call host to start a recording.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await startCallRecording(currentCall.serverUrl, currentCall.channelId);
|
||||
|
||||
return {handled: true};
|
||||
}
|
||||
|
||||
if (action === 'stop') {
|
||||
if (!recording || recording.end_at > recording.start_at) {
|
||||
return {
|
||||
error: intl.formatMessage({
|
||||
id: 'mobile.calls_recording_stop_none_in_progress',
|
||||
defaultMessage: 'No recording is in progress.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (!isHost) {
|
||||
return {
|
||||
error: intl.formatMessage({
|
||||
id: 'mobile.calls_recording_stop_no_permissions',
|
||||
defaultMessage: 'You don\'t have permissions to stop the recording. Please ask the call host to stop the recording.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
await stopCallRecording(currentCall.serverUrl, currentCall.channelId);
|
||||
|
||||
return {handled: true};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {handled: false};
|
||||
|
||||
@@ -13,8 +13,6 @@ export {
|
||||
unraiseHand,
|
||||
setSpeakerphoneOn,
|
||||
handleCallsSlashCommand,
|
||||
startCallRecording,
|
||||
stopCallRecording,
|
||||
} from './calls';
|
||||
|
||||
export {hasMicrophonePermission} from './permissions';
|
||||
|
||||
@@ -23,18 +23,12 @@ import {isSystemAdmin} from '@utils/user';
|
||||
import type {LimitRestrictedInfo} from '@calls/observers';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
// Only unlock when:
|
||||
// - Joining a new call.
|
||||
// - A new recording has started.
|
||||
// - Host has changed to current user.
|
||||
let recordingAlertLock = true;
|
||||
// Only allow one recording alert per call.
|
||||
let recordingAlertLock = false;
|
||||
|
||||
// Only unlock if/when the user starts a recording.
|
||||
let recordingWillBePostedLock = true;
|
||||
|
||||
// Only unlock when starting/stopping a recording.
|
||||
let recordingErrorLock = true;
|
||||
|
||||
export const showLimitRestrictedAlert = (info: LimitRestrictedInfo, intl: IntlShape) => {
|
||||
const title = intl.formatMessage({
|
||||
id: 'mobile.calls_participant_limit_title_GA',
|
||||
@@ -203,7 +197,11 @@ const doJoinCall = async (
|
||||
}
|
||||
|
||||
if (joinChannelIsDMorGM) {
|
||||
unmuteMyself();
|
||||
// FIXME (MM-46048) - HACK
|
||||
// There's a race condition between unmuting and receiving existing tracks from other participants.
|
||||
// Fixing this properly requires extensive and potentially breaking changes.
|
||||
// Waiting for a second before unmuting is a decent workaround that should work in most cases.
|
||||
setTimeout(() => unmuteMyself(), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -226,10 +224,6 @@ const contactAdminAlert = ({formatMessage}: IntlShape) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const needsRecordingAlert = () => {
|
||||
recordingAlertLock = false;
|
||||
};
|
||||
|
||||
export const recordingAlert = (isHost: boolean, intl: IntlShape) => {
|
||||
if (recordingAlertLock) {
|
||||
return;
|
||||
@@ -317,33 +311,3 @@ export const recordingWillBePostedAlert = (intl: IntlShape) => {
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
export const needsRecordingErrorAlert = () => {
|
||||
recordingErrorLock = false;
|
||||
};
|
||||
|
||||
export const recordingErrorAlert = (intl: IntlShape) => {
|
||||
if (recordingErrorLock) {
|
||||
return;
|
||||
}
|
||||
recordingErrorLock = true;
|
||||
|
||||
const {formatMessage} = intl;
|
||||
|
||||
Alert.alert(
|
||||
formatMessage({
|
||||
id: 'mobile.calls_host_rec_error_title',
|
||||
defaultMessage: 'Something went wrong with the recording',
|
||||
}),
|
||||
formatMessage({
|
||||
id: 'mobile.calls_host_rec_error',
|
||||
defaultMessage: 'Please try to record again. You can also contact your system admin for troubleshooting help.',
|
||||
}),
|
||||
[{
|
||||
text: formatMessage({
|
||||
id: 'mobile.calls_dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
}),
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, Pressable, Platform} from 'react-native';
|
||||
|
||||
import {muteMyself, unmuteMyself} from '@calls/actions';
|
||||
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
|
||||
import {recordingAlert, recordingWillBePostedAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import PermissionErrorBar from '@calls/components/permission_error_bar';
|
||||
import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
@@ -158,11 +158,6 @@ const CurrentCallBar = ({
|
||||
recordingWillBePostedAlert(intl);
|
||||
}
|
||||
|
||||
// The host should receive an alert in case of unexpected error.
|
||||
if (isHost && currentCall?.recState?.err) {
|
||||
recordingErrorAlert(intl);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={style.wrapper}>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import moment from 'moment-timezone';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||
|
||||
@@ -25,19 +24,16 @@ const enhanced = withObservables(['serverUrl', 'channelId'], ({
|
||||
channelId,
|
||||
database,
|
||||
}: OwnProps & WithDatabaseArgs) => {
|
||||
const callsState = observeCallsState(serverUrl).pipe(
|
||||
switchMap((state) => of$(state.calls[channelId])),
|
||||
);
|
||||
const callsState = observeCallsState(serverUrl);
|
||||
const participants = callsState.pipe(
|
||||
switchMap((state) => of$(state.calls[channelId])),
|
||||
distinctUntilChanged((prev, curr) => prev?.participants === curr?.participants), // Did the participants object ref change?
|
||||
switchMap((call) => (call ? of$(Object.keys(call.participants)) : of$([]))),
|
||||
distinctUntilChanged((prev, curr) => idsAreEqual(prev, curr)), // Continue only if we have a different set of participant ids
|
||||
switchMap((ids) => (ids.length > 0 ? queryUsersById(database, ids).observeWithColumns(['last_picture_update']) : of$([]))),
|
||||
);
|
||||
const channelCallStartTime = callsState.pipe(
|
||||
|
||||
// if for some reason we don't have a startTime, use 'a few seconds ago' instead of '53 years ago'
|
||||
switchMap((state) => of$(state && state.startTime ? state.startTime : moment.now())),
|
||||
switchMap((cs) => of$(cs.calls[channelId]?.startTime || 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
|
||||
@@ -234,10 +234,8 @@ export async function newConnection(
|
||||
});
|
||||
|
||||
peer.on('stream', (remoteStream: MediaStream) => {
|
||||
logDebug('new remote stream received', remoteStream.id);
|
||||
for (const track of remoteStream.getTracks()) {
|
||||
logDebug('remote track', track.id);
|
||||
}
|
||||
logDebug('new remote stream received', remoteStream);
|
||||
logDebug('remote tracks', remoteStream.getTracks());
|
||||
|
||||
streams.push(remoteStream);
|
||||
if (remoteStream.getVideoTracks().length > 0) {
|
||||
|
||||
@@ -7,15 +7,12 @@ import {EventEmitter} from 'events';
|
||||
import {
|
||||
MediaStream,
|
||||
MediaStreamTrack,
|
||||
RTCIceCandidate,
|
||||
RTCPeerConnection,
|
||||
RTCPeerConnectionIceEvent,
|
||||
RTCRtpSender,
|
||||
RTCSessionDescription,
|
||||
} from 'react-native-webrtc';
|
||||
|
||||
import {logDebug, logError} from '@utils/log';
|
||||
|
||||
import type {RTCPeerConfig} from './types';
|
||||
import type RTCTrackEvent from 'react-native-webrtc/lib/typescript/RTCTrackEvent';
|
||||
|
||||
@@ -24,11 +21,16 @@ const rtcConnFailedErr = new Error('rtc connection failed');
|
||||
export default class RTCPeer extends EventEmitter {
|
||||
private pc: RTCPeerConnection | null;
|
||||
private readonly senders: { [key: string]: RTCRtpSender };
|
||||
private candidates: RTCIceCandidate[] = [];
|
||||
private makingOffer = false;
|
||||
|
||||
public connected: boolean;
|
||||
|
||||
private readonly sessionConstraints = {
|
||||
mandatory: {
|
||||
OfferToReceiveAudio: true,
|
||||
OfferToReceiveVideo: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(config: RTCPeerConfig) {
|
||||
super();
|
||||
|
||||
@@ -85,13 +87,11 @@ export default class RTCPeer extends EventEmitter {
|
||||
|
||||
private async onNegotiationNeeded() {
|
||||
try {
|
||||
this.makingOffer = true;
|
||||
await this.pc?.setLocalDescription();
|
||||
const desc = await this.pc?.createOffer(this.sessionConstraints) as RTCSessionDescription;
|
||||
await this.pc?.setLocalDescription(desc);
|
||||
this.emit('offer', this.pc?.localDescription);
|
||||
} catch (err) {
|
||||
this.emit('error', err);
|
||||
} finally {
|
||||
this.makingOffer = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,38 +110,18 @@ export default class RTCPeer extends EventEmitter {
|
||||
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
if (msg.type === 'offer' && (this.makingOffer || this.pc?.signalingState !== 'stable')) {
|
||||
logDebug('signaling conflict, we are polite, proceeding...');
|
||||
}
|
||||
|
||||
try {
|
||||
switch (msg.type) {
|
||||
case 'candidate':
|
||||
// It's possible that ICE candidates are received moments before
|
||||
// we set the initial remote description which would cause an
|
||||
// error. In such case we queue them up to be added later.
|
||||
if (this.pc.remoteDescription && this.pc.remoteDescription.type) {
|
||||
this.pc.addIceCandidate(msg.candidate).catch((err) => {
|
||||
logError('failed to add candidate', err);
|
||||
});
|
||||
} else {
|
||||
logDebug('received ice candidate before remote description, queuing...');
|
||||
this.candidates.push(msg.candidate);
|
||||
}
|
||||
await this.pc.addIceCandidate(msg.candidate);
|
||||
break;
|
||||
case 'offer':
|
||||
await this.pc.setRemoteDescription(new RTCSessionDescription(msg));
|
||||
await this.pc.setLocalDescription();
|
||||
await this.pc.setLocalDescription(await this.pc.createAnswer() as RTCSessionDescription);
|
||||
this.emit('answer', this.pc.localDescription);
|
||||
break;
|
||||
case 'answer':
|
||||
await this.pc.setRemoteDescription(msg);
|
||||
for (const candidate of this.candidates) {
|
||||
logDebug('adding queued ice candidate');
|
||||
this.pc.addIceCandidate(candidate).catch((err) => {
|
||||
logError('failed to add candidate', err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
this.emit('error', Error('invalid signaling data received'));
|
||||
|
||||
@@ -18,9 +18,10 @@ import {Navigation} from 'react-native-navigation';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import {RTCView} from 'react-native-webrtc';
|
||||
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions';
|
||||
import {startCallRecording, stopCallRecording} from '@calls/actions/calls';
|
||||
import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts';
|
||||
import {recordingAlert, recordingWillBePostedAlert} from '@calls/alerts';
|
||||
import CallAvatar from '@calls/components/call_avatar';
|
||||
import CallDuration from '@calls/components/call_duration';
|
||||
import CallsBadge, {CallsBadgeType} from '@calls/components/calls_badge';
|
||||
@@ -40,7 +41,6 @@ import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {
|
||||
bottomSheet,
|
||||
dismissAllModalsAndPopToScreen,
|
||||
@@ -357,7 +357,7 @@ const CallScreen = ({
|
||||
await popTopScreen(Screens.THREAD);
|
||||
}
|
||||
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
|
||||
WebsocketManager.initializeClient(currentCall.serverUrl);
|
||||
await appEntry(currentCall.serverUrl, Date.now());
|
||||
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
|
||||
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);
|
||||
|
||||
@@ -375,11 +375,6 @@ const CallScreen = ({
|
||||
recordingWillBePostedAlert(intl);
|
||||
}
|
||||
|
||||
// The host should receive an alert in case of unexpected error.
|
||||
if (isHost && currentCall?.recState?.err) {
|
||||
recordingErrorAlert(intl);
|
||||
}
|
||||
|
||||
// The user should see the loading only if:
|
||||
// - Recording has been initialized, recording has not been started, and recording has not ended
|
||||
const waitingForRecording = Boolean(currentCall?.recState?.init_at && !currentCall.recState.start_at && !currentCall.recState.end_at && isHost);
|
||||
|
||||
@@ -5,7 +5,6 @@ import assert from 'assert';
|
||||
|
||||
import {act, renderHook} from '@testing-library/react-hooks';
|
||||
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
newCurrentCall,
|
||||
setCallsState,
|
||||
@@ -54,8 +53,6 @@ import {
|
||||
RecordingState,
|
||||
} from '../types/calls';
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
|
||||
const call1: Call = {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
@@ -152,6 +149,7 @@ describe('useCallsState', () => {
|
||||
|
||||
const expectedCallsState = {
|
||||
...initialCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myId',
|
||||
calls: {'channel-1': testNewCall1, 'channel-2': call2, 'channel-3': call3},
|
||||
enabled: {'channel-2': true},
|
||||
@@ -757,6 +755,7 @@ describe('useCallsState', () => {
|
||||
it('voiceOn and Off', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
@@ -956,8 +955,6 @@ describe('useCallsState', () => {
|
||||
act(() => setRecordingState('server1', 'channel-2', recState));
|
||||
assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, recState});
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
act(() => setRecordingState('server1', 'channel-1', {...recState, start_at: recState.start_at + 1}));
|
||||
expect(needsRecordingAlert).toBeCalled();
|
||||
});
|
||||
|
||||
it('setHost', () => {
|
||||
@@ -1009,7 +1006,5 @@ describe('useCallsState', () => {
|
||||
act(() => setHost('server1', 'channel-2', 'user-1923'));
|
||||
assert.deepEqual((result.current[0] as CallsState).calls['channel-2'], {...call2, hostId: 'user-1923'});
|
||||
assert.deepEqual((result.current[1] as CurrentCall | null), expectedCurrentCallState);
|
||||
act(() => setHost('server1', 'channel-1', 'myUserId'));
|
||||
expect(needsRecordingAlert).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
@@ -35,7 +34,7 @@ export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<
|
||||
}, {} as ChannelsWithCalls);
|
||||
setChannelsWithCalls(serverUrl, channelsWithCalls);
|
||||
|
||||
setCallsState(serverUrl, {myUserId, calls, enabled});
|
||||
setCallsState(serverUrl, {serverUrl, myUserId, calls, enabled});
|
||||
|
||||
// Does the current call need to be updated?
|
||||
const currentCall = getCurrentCall();
|
||||
@@ -514,11 +513,6 @@ export const setRecordingState = (serverUrl: string, channelId: string, recState
|
||||
return;
|
||||
}
|
||||
|
||||
// If a new call has started, we reset the alert state so it can be showed again.
|
||||
if (currentCall.recState && recState.start_at > currentCall.recState.start_at) {
|
||||
needsRecordingAlert();
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
recState,
|
||||
@@ -542,11 +536,6 @@ export const setHost = (serverUrl: string, channelId: string, hostId: string) =>
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are the new host we show the alert again.
|
||||
if (currentCall.myUserId === hostId) {
|
||||
needsRecordingAlert();
|
||||
}
|
||||
|
||||
const nextCurrentCall = {
|
||||
...currentCall,
|
||||
hostId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user