Compare commits

..

9 Commits

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

added record_action button

* wiring components

some more components

update condition

* animated sound wave

sound wave animation - nearly there

* animated microphone

animated microphone in progress

* fix wiring

fix wiring

fix wiring

* PR review fix

* voice record upload

voice record upload

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

voice upload ui [ IN PROGRESS ]

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

* Cleanup and fix errors

* Minor refactoring and add config

* Fix lint

* Add selection logic to post body

* Fix naming
2022-09-21 23:06:03 +02:00
564 changed files with 13669 additions and 23833 deletions

View File

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

View File

@@ -1,31 +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
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
View File

@@ -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

View File

@@ -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.

View File

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

View File

@@ -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,8 +112,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 461
versionName "2.1.0"
versionCode 453
versionName "2.0.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,14 @@ dependencies {
implementation jscFlavor
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
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 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
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 +220,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'

View File

@@ -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 {

View File

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

View File

@@ -1,49 +0,0 @@
package com.mattermost.helpers
import android.graphics.Bitmap
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()
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.byteCount / 1024
}
}
keysCache = LruCache<String, String>(50)
}
fun bitmap(userId: String, updatedAt: Double, serverUrl: String): Bitmap? {
val key = "$serverUrl-$userId-$updatedAt"
return memoryCache.get(key)
}
fun insertBitmap(bitmap: Bitmap?, userId: String, updatedAt: Double, serverUrl: String) {
if (bitmap == null) {
removeBitmap(userId, serverUrl)
}
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()
}
}

View File

@@ -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];
}

View File

@@ -7,6 +7,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -21,15 +22,14 @@ 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;
import androidx.core.app.RemoteInput;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.ReactApplicationContext;
import com.mattermost.rnbeta.*;
import com.nozbe.watermelondb.Database;
import java.io.IOException;
import java.util.Date;
@@ -39,9 +39,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";
@@ -56,10 +53,6 @@ public class CustomPushNotificationHelper {
private static NotificationChannel mHighImportanceChannel;
private static NotificationChannel mMinImportanceChannel;
private static final OkHttpClient client = new OkHttpClient();
private static final BitmapCache bitmapCache = new BitmapCache();
private static void addMessagingStyleMessages(Context context, NotificationCompat.MessagingStyle messagingStyle, String conversationTitle, Bundle bundle) {
String message = bundle.getString("message", bundle.getString("body"));
String senderId = bundle.getString("sender_id");
@@ -128,7 +121,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");
@@ -190,7 +182,7 @@ public class CustomPushNotificationHelper {
setNotificationGroup(notification, groupId, createSummary);
setNotificationBadgeType(notification);
setNotificationChannel(context, notification);
setNotificationChannel(notification, bundle);
setNotificationDeleteIntent(context, notification, bundle, notificationId);
addNotificationReplyAction(context, notification, bundle, notificationId);
@@ -262,6 +254,14 @@ public class CustomPushNotificationHelper {
return title;
}
private static int getIconResourceId(Context context, String iconName) {
final Resources res = context.getResources();
String packageName = context.getPackageName();
String defType = "mipmap";
return res.getIdentifier(iconName, defType, packageName);
}
private static NotificationCompat.MessagingStyle getMessagingStyle(Context context, Bundle bundle) {
NotificationCompat.MessagingStyle messagingStyle;
final String senderId = "me";
@@ -315,6 +315,25 @@ public class CustomPushNotificationHelper {
return getConversationTitle(bundle);
}
public static int getSmallIconResourceId(Context context, String iconName) {
if (iconName == null) {
iconName = "ic_notification";
}
int resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
iconName = "ic_launcher";
resourceId = getIconResourceId(context, iconName);
if (resourceId == 0) {
resourceId = android.R.drawable.ic_dialog_info;
}
}
return resourceId;
}
private static String removeSenderNameFromMessage(String message, String senderName) {
int index = message.indexOf(senderName);
if (index == 0) {
@@ -346,15 +365,12 @@ public class CustomPushNotificationHelper {
}
}
private static void setNotificationChannel(Context context, NotificationCompat.Builder notification) {
private static void setNotificationChannel(NotificationCompat.Builder notification, Bundle bundle) {
// If Android Oreo or above we need to register a channel
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (mHighImportanceChannel == null) {
createNotificationChannels(context);
}
NotificationChannel notificationChannel = mHighImportanceChannel;
notification.setChannelId(notificationChannel.getId());
}
@@ -385,12 +401,14 @@ public class CustomPushNotificationHelper {
}
private static void setNotificationIcons(Context context, NotificationCompat.Builder notification, Bundle bundle) {
String smallIcon = bundle.getString("smallIcon");
String channelName = getConversationTitle(bundle);
String senderName = bundle.getString("sender_name");
String serverUrl = bundle.getString("server_url");
String urlOverride = bundle.getString("override_icon_url");
notification.setSmallIcon(R.mipmap.ic_notification);
int smallIconResId = getSmallIconResourceId(context, smallIcon);
notification.setSmallIcon(smallIconResId);
if (serverUrl != null && channelName.equals(senderName)) {
try {
@@ -405,45 +423,29 @@ 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(Context context, final String serverUrl, final String userId, final String urlOverride) throws IOException {
try {
Response response;
Double lastUpdateAt = 0.0;
final OkHttpClient client = new OkHttpClient();
Request request;
String url;
if (!TextUtils.isEmpty(urlOverride)) {
Request request = new Request.Builder().url(urlOverride).build();
request = new Request.Builder().url(urlOverride).build();
Log.i("ReactNative", String.format("Fetch override profile image %s", urlOverride));
response = client.newCall(request).execute();
} else {
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);
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);
final ReactApplicationContext reactApplicationContext = new ReactApplicationContext(context);
final String token = Credentials.getCredentialsForServerSync(reactApplicationContext, serverUrl);
url = String.format("%s/api/v4/users/%s/image", serverUrl, userId);
Log.i("ReactNative", String.format("Fetch profile image %s", url));
response = Network.getSync(serverUrl, url, null);
request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.url(url)
.build();
}
Response response = client.newCall(request).execute();
if (response.code() == 200) {
assert response.body() != null;
byte[] bytes = Objects.requireNonNull(response.body()).bytes();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (TextUtils.isEmpty(urlOverride) && !TextUtils.isEmpty(userId)) {
bitmapCache.insertBitmap(bitmap.copy(bitmap.getConfig(), false), userId, lastUpdateAt, serverUrl);
}
return getCircleBitmap(bitmap);
}

View File

@@ -2,30 +2,31 @@ 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)
}
}
} catch (e: Exception) {
e.printStackTrace()
// let it fall to return null
val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''"
val cursor = defaultDatabase!!.rawQuery(query)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
return null
}
@@ -36,13 +37,624 @@ class DatabaseHelper {
}
}
fun getServerUrlForIdentifier(identifier: String): String? {
val args: Array<Any?> = arrayOf(identifier)
val query = "SELECT url FROM Servers WHERE identifier=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val url = cursor.getString(0)
cursor.close()
return url
}
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) {
return null
}
}
fun getDatabaseForServer(context: Context?, serverUrl: String): Database? {
val args: Array<Any?> = arrayOf(serverUrl)
val query = "SELECT db_path FROM Servers WHERE url=?"
val cursor = defaultDatabase!!.rawQuery(query, args)
if (cursor.count == 1) {
cursor.moveToFirst()
val databasePath = cursor.getString(0)
cursor.close()
return Database(databasePath, context!!)
}
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? {
if (db != null) {
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId))
if (cursor1.count == 1) {
cursor1.moveToFirst()
val lastFetchedAt = cursor1.getDouble(0)
cursor1.close()
if (lastFetchedAt == 0.0) {
return queryLastPostCreateAt(db, channelId)
}
return lastFetchedAt
}
}
return 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 +662,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
}
}

View File

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

View File

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

View File

@@ -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")))
}
})
}
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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"
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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")))
}
})
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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))
}
}

View File

@@ -7,34 +7,31 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.helpers.ResolvePromise;
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;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
CustomPushNotificationHelper.createNotificationChannels(context);
dataHelper = new PushNotificationDataHelper(context);
try {
@@ -57,35 +54,30 @@ 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);
if (isIdLoaded && response != null) {
Bundle current = mNotificationProps.asBundle();
if (!current.containsKey("server_url")) {
response.putString("server_url", serverUrl);
notificationReceiptDelivery(ackId, serverUrl, postId, type, isIdLoaded, new ResolvePromise() {
@Override
public void resolve(@Nullable Object value) {
if (isIdLoaded) {
Bundle response = (Bundle) value;
if (value != null) {
response.putString("server_url", serverUrl);
Bundle current = mNotificationProps.asBundle();
current.putAll(response);
mNotificationProps = createProps(current);
}
}
}
current.putAll(response);
mNotificationProps = createProps(current);
}
@Override
public void reject(String code, String message) {
Log.e("ReactNative", code + ": " + message);
}
});
}
finishProcessingNotification(serverUrl, type, channelId, notificationId);
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void finishProcessingNotification(final String serverUrl, @NonNull final String type, final String channelId, final int notificationId) {
final boolean isReactInit = mAppLifecycleFacade.isReactInitialized();
switch (type) {
case CustomPushNotificationHelper.PUSH_TYPE_MESSAGE:
case CustomPushNotificationHelper.PUSH_TYPE_SESSION:
@@ -97,17 +89,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,
@@ -130,6 +118,16 @@ public class CustomPushNotification extends PushNotification {
}
}
@Override
public void onOpened() {
if (mNotificationProps != null) {
digestNotification();
Bundle data = mNotificationProps.asBundle();
NotificationHelper.clearChannelOrThreadNotifications(mContext, data);
}
}
private void buildNotification(Integer notificationId, boolean createSummary) {
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
final Notification notification = buildNotification(pendingIntent);
@@ -151,25 +149,26 @@ public class CustomPushNotification extends PushNotification {
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
}
private void notificationReceiptDelivery(String ackId, String serverUrl, String postId, String type, boolean isIdLoaded, ResolvePromise promise) {
ReceiptDelivery.send(mContext, ackId, serverUrl, postId, type, isIdLoaded, promise);
}
private void notifyReceivedToJS() {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
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;

View File

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

View File

@@ -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);
@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).
@@ -47,23 +43,10 @@ public class MainActivity extends NavigationActivity {
super.onCreate(null);
setContentView(R.layout.launch_screen);
setHWKeyboardConnected();
foldableObserver.onCreate();
}
@Override
protected void onStart() {
super.onStart();
foldableObserver.onStart();
}
@Override
protected void onStop() {
super.onStop();
foldableObserver.onStop();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
@@ -101,7 +84,7 @@ public class MainActivity extends NavigationActivity {
}
}
return super.dispatchKeyEvent(event);
}
};
private void setHWKeyboardConnected() {
HWKeyboardConnected = getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY;

View File

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

View File

@@ -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;
@@ -131,6 +133,19 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
promise.resolve(map);
}
@ReactMethod
public void isRunningInSplitView(final Promise promise) {
WritableMap result = Arguments.createMap();
Activity current = getCurrentActivity();
if (current != null) {
result.putBoolean("isSplitView", current.isInMultiWindowMode());
} else {
result.putBoolean("isSplitView", false);
}
promise.resolve(result);
}
@ReactMethod
public void saveFile(String path, final Promise promise) {
Uri contentUri;
@@ -147,7 +162,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 +189,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 +224,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 +250,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;

View File

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

View File

@@ -1,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;

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

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

View File

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

View File

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

View File

@@ -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};
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');
await operator.batchRecords(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);
}
}
await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId);
const dt = Date.now();
await operator.batchRecords(models);
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) {

View File

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

View File

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

View File

@@ -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);
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};
}

View File

@@ -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 {Preferences, 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 {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {getMyTeamById} from '@queries/servers/team';
import {getIsCRTEnabled} from '@queries/servers/thread';
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;
}
@@ -50,7 +53,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
// When opening the app from a push notification the theme may not be set in the EphemeralStore
// causing the goToScreen to use the Appearance theme instead and that causes the screen background color to potentially
// not match the theme
const themes = await queryThemePreferences(database, teamId).fetch();
const themes = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_THEME, teamId).fetch();
let theme = getDefaultThemeByAppearance();
if (themes.length) {
theme = setThemeDefaults(JSON.parse(themes[0].value) as Theme);
@@ -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);
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};
}

View File

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

View File

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

View File

@@ -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);
}
};

View File

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

View File

@@ -1,21 +1,18 @@
// 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 {queryPreferencesByCategoryAndName} 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';
const {CATEGORY_DIRECT_CHANNEL_SHOW, CATEGORY_GROUP_CHANNEL_SHOW, CATEGORY_FAVORITE_CHANNEL, CATEGORY_SAVED_POST} = Preferences;
export type MyPreferencesRequest = {
preferences?: PreferenceType[];
error?: unknown;
@@ -59,7 +56,7 @@ export const saveFavoriteChannel = async (serverUrl: string, channelId: string,
try {
const userId = await getCurrentUserId(operator.database);
const favPref: PreferenceType = {
category: Preferences.CATEGORIES.FAVORITE_CHANNEL,
category: CATEGORY_FAVORITE_CHANNEL,
name: channelId,
user_id: userId,
value: String(isFavorite),
@@ -80,7 +77,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
const userId = await getCurrentUserId(operator.database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORIES.SAVED_POST,
category: CATEGORY_SAVED_POST,
name: postId,
value: 'true',
};
@@ -119,15 +116,23 @@ export const savePreference = async (serverUrl: string, preferences: PreferenceT
};
export const deleteSavedPost = async (serverUrl: string, postId: string) => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const client = NetworkManager.getClient(serverUrl);
const userId = await getCurrentUserId(database);
const records = await querySavedPostsPreferences(database, postId).fetch();
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const userId = await getCurrentUserId(operator.database);
const records = await queryPreferencesByCategoryAndName(operator.database, CATEGORY_SAVED_POST, postId).fetch();
const postPreferenceRecord = records.find((r) => postId === r.name);
const pref = {
user_id: userId,
category: Preferences.CATEGORIES.SAVED_POST,
category: CATEGORY_SAVED_POST,
name: postId,
value: 'true',
};
@@ -156,8 +161,7 @@ export const setDirectChannelVisible = async (serverUrl: string, channelId: stri
const channel = await getChannelById(database, channelId);
if (channel?.type === General.DM_CHANNEL || channel?.type === General.GM_CHANNEL) {
const userId = await getCurrentUserId(database);
const {DIRECT_CHANNEL_SHOW, GROUP_CHANNEL_SHOW} = Preferences.CATEGORIES;
const category = channel.type === General.DM_CHANNEL ? DIRECT_CHANNEL_SHOW : GROUP_CHANNEL_SHOW;
const category = channel.type === General.DM_CHANNEL ? CATEGORY_DIRECT_CHANNEL_SHOW : CATEGORY_GROUP_CHANNEL_SHOW;
const name = channel.type === General.DM_CHANNEL ? getUserIdFromChannelName(userId, channel.name) : channelId;
const pref: PreferenceType = {
user_id: userId,
@@ -181,7 +185,7 @@ export const savePreferredSkinTone = async (serverUrl: string, skinCode: string)
const userId = await getCurrentUserId(database);
const pref: PreferenceType = {
user_id: userId,
category: Preferences.CATEGORIES.EMOJI,
category: Preferences.CATEGORY_EMOJI,
name: Preferences.EMOJI_SKINTONE,
value: skinCode,
};
@@ -190,11 +194,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);
};

View File

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

View File

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

View File

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

View File

@@ -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};
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
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;
}
}

View File

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

View File

@@ -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.CATEGORY_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
}
}

View File

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

View File

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

View File

@@ -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> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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