forked from Ivasoft/mattermost-mobile
Compare commits
9 Commits
test1.0.4
...
voice-mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ab2f86cb | ||
|
|
79ee4ebb30 | ||
|
|
0db1d556df | ||
|
|
441e710bc1 | ||
|
|
f9b7932920 | ||
|
|
97cd096934 | ||
|
|
081ac80fde | ||
|
|
f9f0e95ce1 | ||
|
|
9854683321 |
@@ -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 }}
|
||||
|
||||
31
.drone.yml
31
.drone.yml
@@ -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
5
.gitignore
vendored
@@ -7,9 +7,6 @@ mattermost.keystore
|
||||
tmp/
|
||||
.env
|
||||
env.d.ts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ipa
|
||||
|
||||
*/**/compass-icons.ttf
|
||||
|
||||
@@ -33,6 +30,8 @@ xcuserdata
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.apk
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
ios/Pods
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Submit feature requests to https://mattermost.com/suggestions/. File non-security related bugs here in the following format:
|
||||
Submit feature requests to https://portal.productboard.com/mattermost/33-what-matters-to-you. File non-security related bugs here in the following format:
|
||||
|
||||
#### Summary
|
||||
Issue in one concise sentence.
|
||||
|
||||
65
NOTICE.txt
65
NOTICE.txt
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,151 +4,292 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import com.facebook.react.bridge.Arguments
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.mattermost.helpers.push_notification.*
|
||||
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class PushNotificationDataHelper(private val context: Context) {
|
||||
private var coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
var result: Bundle? = null
|
||||
val job = coroutineScope.launch(Dispatchers.Default) {
|
||||
result = PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
runBlocking {
|
||||
job.join()
|
||||
}
|
||||
|
||||
return result
|
||||
private var scope = Executors.newSingleThreadExecutor()
|
||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle) {
|
||||
scope.execute(Runnable {
|
||||
runBlocking {
|
||||
PushNotificationDataRunnable.start(context, initialData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PushNotificationDataRunnable {
|
||||
companion object {
|
||||
internal val specialMentions = listOf("all", "here", "channel")
|
||||
private val dbHelper = DatabaseHelper.instance!!
|
||||
private val mutex = Mutex()
|
||||
private val specialMentions = listOf("all", "here", "channel")
|
||||
|
||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||
mutex.withLock {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return null
|
||||
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||
var result: Bundle? = null
|
||||
@Synchronized
|
||||
suspend fun start(context: Context, initialData: Bundle) {
|
||||
try {
|
||||
val serverUrl: String = initialData.getString("server_url") ?: return
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
val db = DatabaseHelper.instance!!.getDatabaseForServer(context, serverUrl)
|
||||
Log.i("ReactNative", "Start fetching notification data in server="+serverUrl+" for channel="+channelId)
|
||||
|
||||
try {
|
||||
if (db != null) {
|
||||
val teamId = initialData.getString("team_id")
|
||||
val channelId = initialData.getString("channel_id")
|
||||
val postId = initialData.getString("post_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||
if (db != null) {
|
||||
var postData: ReadableMap?
|
||||
var posts: ReadableMap? = null
|
||||
var userIdsToLoad: ReadableArray? = null
|
||||
var usernamesToLoad: ReadableArray? = null
|
||||
|
||||
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId")
|
||||
var threads: ReadableArray? = null
|
||||
var usersFromThreads: ReadableArray? = null
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val notificationData = Arguments.createMap()
|
||||
coroutineScope {
|
||||
if (channelId != null) {
|
||||
postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId)
|
||||
|
||||
if (!teamId.isNullOrEmpty()) {
|
||||
val res = fetchTeamIfNeeded(db, serverUrl, teamId)
|
||||
res.first?.let { notificationData.putMap("team", it) }
|
||||
res.second?.let { notificationData.putMap("myTeam", it) }
|
||||
}
|
||||
posts = postData?.getMap("posts")
|
||||
userIdsToLoad = postData?.getArray("userIdsToLoad")
|
||||
usernamesToLoad = postData?.getArray("usernamesToLoad")
|
||||
threads = postData?.getArray("threads")
|
||||
usersFromThreads = postData?.getArray("usersFromThreads")
|
||||
|
||||
if (channelId != null && postId != null) {
|
||||
val channelRes = fetchMyChannel(db, serverUrl, channelId, isCRTEnabled)
|
||||
channelRes.first?.let { notificationData.putMap("channel", it) }
|
||||
channelRes.second?.let { notificationData.putMap("myChannel", it) }
|
||||
val loadedProfiles = channelRes.third
|
||||
|
||||
// Fetch categories if needed
|
||||
if (!teamId.isNullOrEmpty() && notificationData.getMap("myTeam") != null) {
|
||||
// should load all categories
|
||||
val res = fetchMyTeamCategories(db, serverUrl, teamId)
|
||||
res?.let { notificationData.putMap("categories", it) }
|
||||
} else if (notificationData.getMap("channel") != null) {
|
||||
// check if the channel is in the category for the team
|
||||
val res = addToDefaultCategoryIfNeeded(db, notificationData.getMap("channel")!!)
|
||||
res?.let { notificationData.putArray("categoryChannels", it) }
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
val users = fetchUsersById(serverUrl, userIdsToLoad!!)
|
||||
userIdsToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
val postData = fetchPosts(db, serverUrl, channelId, isCRTEnabled, rootId, loadedProfiles)
|
||||
postData?.getMap("posts")?.let { notificationData.putMap("posts", it) }
|
||||
|
||||
var notificationThread: ReadableMap? = null
|
||||
if (isCRTEnabled && !rootId.isNullOrEmpty()) {
|
||||
notificationThread = fetchThread(db, serverUrl, rootId, teamId)
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
val users = fetchUsersByUsernames(serverUrl, usernamesToLoad!!)
|
||||
usernamesToLoad = users?.getArray("data")
|
||||
}
|
||||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for(item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
}
|
||||
|
||||
val userList = fetchNeededUsers(serverUrl, loadedProfiles, postData)
|
||||
notificationData.putArray("users", ReadableArrayUtils.toWritableArray(userList.toArray()))
|
||||
}
|
||||
|
||||
result = Arguments.toBundle(notificationData)
|
||||
|
||||
if (!isReactInit) {
|
||||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||
}
|
||||
|
||||
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
db?.close()
|
||||
Log.i("ReactNative", "DONE fetching notification data")
|
||||
}
|
||||
|
||||
return result
|
||||
db.transaction {
|
||||
if (posts != null && channelId != null) {
|
||||
DatabaseHelper.instance!!.handlePosts(db, posts!!.getMap("data"), channelId, receivingThreads)
|
||||
}
|
||||
|
||||
if (threads != null) {
|
||||
DatabaseHelper.instance!!.handleThreads(db, threads!!)
|
||||
}
|
||||
|
||||
if (userIdsToLoad != null && userIdsToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, userIdsToLoad!!)
|
||||
}
|
||||
|
||||
if (usernamesToLoad != null && usernamesToLoad!!.size() > 0) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usernamesToLoad!!)
|
||||
}
|
||||
|
||||
if (usersFromThreads != null) {
|
||||
DatabaseHelper.instance!!.handleUsers(db, usersFromThreads!!)
|
||||
}
|
||||
}
|
||||
|
||||
db.close()
|
||||
Log.i("ReactNative", "Done processing push notification="+serverUrl+" for channel="+channelId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThreadList(notificationThread: ReadableMap?, threads: ReadableArray?): ArrayList<ReadableMap>? {
|
||||
threads?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
val threadIds = ArrayList<String>()
|
||||
notificationThread?.let { thread ->
|
||||
thread.getString("id")?.let { it1 -> threadIds.add(it1) }
|
||||
threadsArray.add(thread)
|
||||
}
|
||||
for(i in 0 until it.size()) {
|
||||
val thread = it.getMap(i)
|
||||
val threadId = thread.getString("id")
|
||||
if (threadId != null) {
|
||||
if (threadIds.contains(threadId)) {
|
||||
// replace the values for participants and is_following
|
||||
val index = threadsArray.indexOfFirst { el -> el.getString("id") == threadId }
|
||||
val prev = threadsArray[index]
|
||||
val merge = Arguments.createMap()
|
||||
merge.merge(prev)
|
||||
merge.putBoolean("is_following", thread.getBoolean("is_following"))
|
||||
merge.putArray("participants", thread.getArray("participants"))
|
||||
threadsArray[index] = merge
|
||||
} else {
|
||||
threadsArray.add(thread)
|
||||
threadIds.add(threadId)
|
||||
private suspend fun fetchPosts(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean, rootId: String?): ReadableMap? {
|
||||
val regex = Regex("""\B@(([a-z0-9-._]*[a-z0-9_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val since = DatabaseHelper.instance!!.queryPostSinceForChannel(db, channelId)
|
||||
val currentUserId = DatabaseHelper.instance!!.queryCurrentUserId(db)?.removeSurrounding("\"")
|
||||
val currentUser = DatabaseHelper.instance!!.find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val queryParams = "?skipFetchThreads=false&perPage=60&fromCreatedAt=0&direction=up"
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postsResponse != null) {
|
||||
val data = ReadableMapUtils.toMap(postsResponse)
|
||||
results.putMap("posts", postsResponse)
|
||||
val postsData = data["data"] as? Map<*, *>
|
||||
if (postsData != null) {
|
||||
val postsMap = postsData["posts"]
|
||||
if (postsMap != null) {
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
|
||||
while(iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
threads.pushMap(post!!)
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post.getArray("participants")
|
||||
if (participants != null) {
|
||||
for (i in 0 until participants.size()) {
|
||||
val participant = participants.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = DatabaseHelper.instance!!.queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = DatabaseHelper.instance!!.queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = DatabaseHelper.instance!!.queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach{ (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
return threadsArray
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
return null
|
||||
private suspend fun fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableMap? {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
return fetchWithPost(serverUrl, endpoint, options)
|
||||
}
|
||||
|
||||
private suspend fun fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.mattermost.helpers;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableType;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
|
||||
@@ -110,9 +109,7 @@ public class ReadableArrayUtils {
|
||||
writableArray.pushString((String) value);
|
||||
} else if (value instanceof Map) {
|
||||
writableArray.pushMap(ReadableMapUtils.toWritableMap((Map<String, Object>) value));
|
||||
} else if (value instanceof ReadableMap) {
|
||||
writableArray.pushMap((ReadableMap) value);
|
||||
}else if (value.getClass().isArray()) {
|
||||
} else if (value.getClass().isArray()) {
|
||||
writableArray.pushArray(ReadableArrayUtils.toWritableArray((Object[]) value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,37 +122,24 @@ public class RealPathUtil {
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
|
||||
boolean cacheDirExists = cacheDir.exists();
|
||||
if (!cacheDirExists) {
|
||||
cacheDirExists = cacheDir.mkdirs();
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
if (cacheDirExists) {
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
boolean fileCreated = tmpFile.createNewFile();
|
||||
tmpFile = new File(cacheDir, fileName);
|
||||
tmpFile.createNewFile();
|
||||
|
||||
if (fileCreated) {
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
|
||||
try (FileInputStream inputSrc = new FileInputStream(pfd.getFileDescriptor())) {
|
||||
FileChannel src = inputSrc.getChannel();
|
||||
try (FileOutputStream outputDst = new FileOutputStream(tmpFile)) {
|
||||
FileChannel dst = outputDst.getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
}
|
||||
}
|
||||
|
||||
pfd.close();
|
||||
}
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
|
||||
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
|
||||
dst.transferFrom(src, 0, src.size());
|
||||
src.close();
|
||||
dst.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static String getDataColumn(Context context, Uri uri, String selection,
|
||||
@@ -258,9 +245,7 @@ public class RealPathUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileOrDirectory.delete()) {
|
||||
Log.i("ReactNative", "Couldn't delete file " + fileOrDirectory.getName());
|
||||
}
|
||||
fileOrDirectory.delete();
|
||||
}
|
||||
|
||||
private static String sanitizeFilename(String filename) {
|
||||
@@ -271,4 +256,22 @@ public class RealPathUtil {
|
||||
File f = new File(filename);
|
||||
return f.getName();
|
||||
}
|
||||
|
||||
public static File createDirIfNotExists(String path) {
|
||||
File dir = new File(path);
|
||||
if (dir.exists()) {
|
||||
return dir;
|
||||
}
|
||||
|
||||
try {
|
||||
dir.mkdirs();
|
||||
// Add .nomedia to hide the thumbnail directory from gallery
|
||||
File noMedia = new File(path, ".nomedia");
|
||||
noMedia.createNewFile();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun insertCategory(db: Database, category: ReadableMap) {
|
||||
try {
|
||||
val id = category.getString("id") ?: return
|
||||
val collapsed = false
|
||||
val displayName = category.getString("display_name")
|
||||
val muted = category.getBoolean("muted")
|
||||
val sortOrder = category.getInt("sort_order")
|
||||
val sorting = category.getString("sorting") ?: "recent"
|
||||
val teamId = category.getString("team_id")
|
||||
val type = category.getString("type")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Category
|
||||
(id, collapsed, display_name, muted, sort_order, sorting, team_id, type, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, collapsed, displayName, muted,
|
||||
sortOrder / 10, sorting, teamId, type
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoryChannels(db: Database, categoryId: String, teamId: String, channelIds: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until channelIds.size()) {
|
||||
val channelId = channelIds.getString(i)
|
||||
val id = "${teamId}_$channelId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, i)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertCategoriesWithChannels(db: Database, orderCategories: ReadableMap) {
|
||||
val categories = orderCategories.getArray("categories") ?: return
|
||||
for (i in 0 until categories.size()) {
|
||||
val category = categories.getMap(i)
|
||||
val id = category.getString("id")
|
||||
val teamId = category.getString("team_id")
|
||||
val channelIds = category.getArray("channel_ids")
|
||||
insertCategory(db, category)
|
||||
if (id != null && teamId != null) {
|
||||
channelIds?.let { insertCategoryChannels(db, id, teamId, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelToDefaultCategory(db: Database, categoryChannels: ReadableArray) {
|
||||
try {
|
||||
for (i in 0 until categoryChannels.size()) {
|
||||
val cc = categoryChannels.getMap(i)
|
||||
val id = cc.getString("id")
|
||||
val categoryId = cc.getString("category_id")
|
||||
val channelId = cc.getString("channel_id")
|
||||
val count = countByColumn(db, "CategoryChannel", "category_id", categoryId)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO CategoryChannel
|
||||
(id, category_id, channel_id, sort_order, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, categoryId, channelId, if (count > 0) count + 1 else count)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
fun findChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Channel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyChannel(db: Database?, channelId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyChannel", channelId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun handleChannel(db: Database, channel: ReadableMap) {
|
||||
try {
|
||||
val exists = channel.getString("id")?.let { findChannel(db, it) } ?: false
|
||||
if (!exists) {
|
||||
val json = ReadableMapUtils.toJSONObject(channel)
|
||||
if (insertChannel(db, json)) {
|
||||
insertChannelInfo(db, json)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun DatabaseHelper.handleMyChannel(db: Database, myChannel: ReadableMap, postsData: ReadableMap?, receivingThreads: Boolean) {
|
||||
try {
|
||||
val json = ReadableMapUtils.toJSONObject(myChannel)
|
||||
val exists = myChannel.getString("id")?.let { findMyChannel(db, it) } ?: false
|
||||
|
||||
if (postsData != null && !receivingThreads) {
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val postList = posts.toList()
|
||||
val lastFetchedAt = postList.fold(0.0) { acc, next ->
|
||||
val post = next.second as Map<*, *>
|
||||
val createAt = post["create_at"] as Double
|
||||
val updateAt = post["update_at"] as Double
|
||||
val deleteAt = post["delete_at"] as Double
|
||||
val value = maxOf(createAt, updateAt, deleteAt)
|
||||
|
||||
maxOf(value, acc)
|
||||
}
|
||||
json.put("last_fetched_at", lastFetchedAt)
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
updateMyChannel(db, json)
|
||||
return
|
||||
}
|
||||
|
||||
if (insertMyChannel(db, json)) {
|
||||
insertMyChannelSettings(db, json)
|
||||
insertChannelMember(db, json)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannel(db: Database, channel: JSONObject): Boolean {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return false }
|
||||
val createAt = try { channel.getDouble("create_at") } catch (e: JSONException) { 0 }
|
||||
val deleteAt = try { channel.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { channel.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val creatorId = try { channel.getString("creator_id") } catch (e: JSONException) { "" }
|
||||
val displayName = try { channel.getString("display_name") } catch (e: JSONException) { "" }
|
||||
val name = try { channel.getString("name") } catch (e: JSONException) { "" }
|
||||
val teamId = try { channel.getString("team_id") } catch (e: JSONException) { "" }
|
||||
val type = try { channel.getString("type") } catch (e: JSONException) { "O" }
|
||||
val isGroupConstrained = try { channel.getBoolean("group_constrained") } catch (e: JSONException) { false }
|
||||
val shared = try { channel.getBoolean("shared") } catch (e: JSONException) { false }
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Channel
|
||||
(id, create_at, delete_at, update_at, creator_id, display_name, name, team_id, type, is_group_constrained, shared, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, createAt, deleteAt, updateAt,
|
||||
creatorId, displayName, name, teamId, type,
|
||||
isGroupConstrained, shared
|
||||
)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelInfo(db: Database, channel: JSONObject) {
|
||||
val id = try { channel.getString("id") } catch (e: JSONException) { return }
|
||||
val header = try { channel.getString("header") } catch (e: JSONException) { "" }
|
||||
val purpose = try { channel.getString("purpose") } catch (e: JSONException) { "" }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelInfo
|
||||
(id, header, purpose, guest_count, member_count, pinned_post_count, _changed, _status)
|
||||
VALUES (?, ?, ?, 0, 0, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, header, purpose)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannel(db: Database, myChanel: JSONObject): Boolean {
|
||||
return try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return false }
|
||||
val roles = try { myChanel.getString("roles") } catch (e: JSONException) { "" }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val viewedAt = 0
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
val manuallyUnread = false
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannel
|
||||
(id, roles, message_count, mentions_count, is_unread, manually_unread,
|
||||
last_post_at, last_viewed_at, viewed_at, last_fetched_at, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(
|
||||
id, roles, msgCount, mentionsCount, isUnread, manuallyUnread,
|
||||
lastPostAt, lastViewedAt, viewedAt, lastFetchedAt
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyChannelSettings(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val notifyProps = try { myChanel.getString("notify_props") } catch (e: JSONException) { return }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO MyChannelSettings (id, notify_props, _changed, _status)
|
||||
VALUES (?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, notifyProps)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun insertChannelMember(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val userId = queryCurrentUserId(db) ?: return
|
||||
val channelId = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val schemeAdmin = try { myChanel.getBoolean("scheme_admin") } catch (e: JSONException) { false }
|
||||
val id = "$channelId-$userId"
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ChannelMembership
|
||||
(id, channel_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""",
|
||||
arrayOf(id, channelId, userId, schemeAdmin)
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMyChannel(db: Database, myChanel: JSONObject) {
|
||||
try {
|
||||
val id = try { myChanel.getString("id") } catch (e: JSONException) { return }
|
||||
val msgCount = try { myChanel.getInt("message_count") } catch (e: JSONException) { 0 }
|
||||
val mentionsCount = try { myChanel.getInt("mentions_count") } catch (e: JSONException) { 0 }
|
||||
val isUnread = try { myChanel.getBoolean("is_unread") } catch (e: JSONException) { false }
|
||||
val lastPostAt = try { myChanel.getDouble("last_post_at") } catch (e: JSONException) { 0 }
|
||||
val lastViewedAt = try { myChanel.getDouble("last_viewed_at") } catch (e: JSONException) { 0 }
|
||||
val lastFetchedAt = try { myChanel.getDouble("last_fetched_at") } catch (e: JSONException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE MyChannel SET message_count=?, mentions_count=?, is_unread=?,
|
||||
last_post_at=?, last_viewed_at=?, last_fetched_at=?, _status = 'updated'
|
||||
WHERE id=?
|
||||
""",
|
||||
arrayOf(
|
||||
msgCount, mentionsCount, isUnread,
|
||||
lastPostAt, lastViewedAt, lastFetchedAt, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertCustomEmojis(db: Database, customEmojis: JSONArray) {
|
||||
for (i in 0 until customEmojis.length()) {
|
||||
try {
|
||||
val emoji = customEmojis.getJSONObject(i)
|
||||
if (find(db, "CustomEmoji", emoji.getString("id")) == null) {
|
||||
db.execute(
|
||||
"INSERT INTO CustomEmoji (id, name, _changed, _status) VALUES (?, ?, '', 'created')",
|
||||
arrayOf(
|
||||
emoji.getString("id"),
|
||||
emoji.getString("name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
internal fun insertFiles(db: Database, files: JSONArray) {
|
||||
try {
|
||||
for (i in 0 until files.length()) {
|
||||
val file = files.getJSONObject(i)
|
||||
val id = file.getString("id")
|
||||
val extension = file.getString("extension")
|
||||
val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }
|
||||
val height = try { file.getInt("height") } catch (e: JSONException) { 0 }
|
||||
val mime = file.getString("mime_type")
|
||||
val name = file.getString("name")
|
||||
val postId = file.getString("post_id")
|
||||
val size = try { file.getDouble("size") } catch (e: JSONException) { 0 }
|
||||
val width = try { file.getInt("width") } catch (e: JSONException) { 0 }
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO File
|
||||
(id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, extension, height, miniPreview,
|
||||
mime, name, postId, size, width
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.QueryArgs
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import java.util.*
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: Database, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
val posts = data.getMap("posts")
|
||||
data.getMap("team")?.let { insertTeam(db, it) }
|
||||
data.getMap("myTeam")?.let { insertMyTeam(db, it) }
|
||||
data.getMap("channel")?.let { handleChannel(db, it) }
|
||||
data.getMap("myChannel")?.let { handleMyChannel(db, it, posts, receivingThreads) }
|
||||
data.getMap("categories")?.let { insertCategoriesWithChannels(db, it) }
|
||||
data.getArray("categoryChannels")?.let { insertChannelToDefaultCategory(db, it) }
|
||||
if (channelId != null) {
|
||||
handlePosts(db, posts, channelId, receivingThreads)
|
||||
}
|
||||
data.getArray("threads")?.let {
|
||||
val threadsArray = ArrayList<ReadableMap>()
|
||||
for (i in 0 until it.size()) {
|
||||
threadsArray.add(it.getMap(i))
|
||||
}
|
||||
handleThreads(db, threadsArray, teamId)
|
||||
}
|
||||
data.getArray("users")?.let { handleUsers(db, it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getServerUrlForIdentifier(identifier: String): String? {
|
||||
try {
|
||||
val query = "SELECT url FROM Servers WHERE identifier=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(identifier)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): Database? {
|
||||
try {
|
||||
val query = "SELECT db_path FROM Servers WHERE url=?"
|
||||
defaultDatabase!!.rawQuery(query, arrayOf(serverUrl)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val databasePath = cursor.getString(0)
|
||||
return Database(databasePath, context!!)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun find(db: Database, tableName: String, id: String?): ReadableMap? {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE id == ? LIMIT 1",
|
||||
arrayOf(id)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun findByColumns(db: Database, tableName: String, columnNames: Array<String>, values: QueryArgs): ReadableMap? {
|
||||
try {
|
||||
val whereString = columnNames.joinToString(" AND ") { "$it = ?" }
|
||||
db.rawQuery(
|
||||
"SELECT * FROM $tableName WHERE $whereString LIMIT 1",
|
||||
values
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return null
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun queryIds(db: Database, tableName: String, ids: Array<String>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(ids).map { "?" }.toArray())
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
db.rawQuery("SELECT DISTINCT id FROM $tableName WHERE id IN ($args)", ids as Array<Any?>).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex("id")
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array<Any?>): List<String> {
|
||||
val list: MutableList<String> = ArrayList()
|
||||
val args = TextUtils.join(",", Arrays.stream(values).map { "?" }.toArray())
|
||||
try {
|
||||
db.rawQuery("SELECT DISTINCT $columnName FROM $tableName WHERE $columnName IN ($args)", values).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
while (cursor.moveToNext()) {
|
||||
val index = cursor.getColumnIndex(columnName)
|
||||
if (index >= 0) {
|
||||
list.add(cursor.getString(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun countByColumn(db: Database, tableName: String, columnName: String, value: Any?): Int {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT COUNT(*) FROM $tableName WHERE $columnName == ? LIMIT 1",
|
||||
arrayOf(value)
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return 0
|
||||
}
|
||||
cursor.moveToFirst()
|
||||
return cursor.getInt(0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.DatabaseHelper
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.Exception
|
||||
|
||||
internal fun queryLastPostCreateAt(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor1 ->
|
||||
if (cursor1.count == 1) {
|
||||
cursor1.moveToFirst()
|
||||
val earliest = cursor1.getDouble(0)
|
||||
val latest = cursor1.getDouble(1)
|
||||
val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC"
|
||||
|
||||
db.rawQuery(postQuery, arrayOf(channelId, earliest, latest)).use { cursor2 ->
|
||||
if (cursor2.count >= 60) {
|
||||
cursor2.moveToFirst()
|
||||
return cursor2.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryPostSinceForChannel(db: Database?, channelId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val postsInChannelQuery = "SELECT last_fetched_at FROM MyChannel WHERE id=? LIMIT 1"
|
||||
db.rawQuery(postsInChannelQuery, arrayOf(channelId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val lastFetchedAt = cursor.getDouble(0)
|
||||
if (lastFetchedAt == 0.0) {
|
||||
return queryLastPostCreateAt(db, channelId)
|
||||
}
|
||||
return lastFetchedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// let it fall to return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun queryLastPostInThread(db: Database?, rootId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
val query = "SELECT create_at FROM Post WHERE root_id=? AND delete_at=0 ORDER BY create_at DESC LIMIT 1"
|
||||
db.rawQuery(query, arrayOf(rootId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
val files = metadata.remove("files") as JSONArray?
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Post
|
||||
(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id,
|
||||
previous_post_id, root_id, type, user_id, props, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
|
||||
if (files != null && files.length() > 0) {
|
||||
insertFiles(db, files)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updatePost(db: Database, post: JSONObject) {
|
||||
try {
|
||||
val id = try { post.getString("id") } catch (e: JSONException) { return }
|
||||
val channelId = try { post.getString("channel_id") } catch (e: JSONException) { return }
|
||||
val userId = try { post.getString("user_id") } catch (e: JSONException) { return }
|
||||
val createAt = try { post.getDouble("create_at") } catch (e: JSONException) { return }
|
||||
val deleteAt = try { post.getDouble("delete_at") } catch (e: JSONException) { 0 }
|
||||
val updateAt = try { post.getDouble("update_at") } catch (e: JSONException) { 0 }
|
||||
val editAt = try { post.getDouble("edit_at") } catch (e: JSONException) { 0 }
|
||||
val isPinned = try { post.getBoolean("is_pinned") } catch (e: JSONException) { false }
|
||||
val message = try { post.getString("message") } catch (e: JSONException) { "" }
|
||||
val metadata = try { post.getJSONObject("metadata") } catch (e: JSONException) { JSONObject() }
|
||||
val originalId = try { post.getString("original_id") } catch (e: JSONException) { "" }
|
||||
val pendingId = try { post.getString("pending_post_id") } catch (e: JSONException) { "" }
|
||||
val prevId = try { post.getString("prev_post_id") } catch (e: JSONException) { "" }
|
||||
val rootId = try { post.getString("root_id") } catch (e: JSONException) { "" }
|
||||
val type = try { post.getString("type") } catch (e: JSONException) { "" }
|
||||
val props = try { post.getJSONObject("props").toString() } catch (e: JSONException) { "" }
|
||||
val reactions = metadata.remove("reactions") as JSONArray?
|
||||
val customEmojis = metadata.remove("emojis") as JSONArray?
|
||||
|
||||
metadata.remove("files")
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?,
|
||||
is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?,
|
||||
root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated'
|
||||
WHERE id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
channelId, createAt, deleteAt, updateAt, editAt,
|
||||
isPinned, message, metadata.toString(),
|
||||
originalId, pendingId, prevId, rootId,
|
||||
type, userId, props,
|
||||
id,
|
||||
)
|
||||
)
|
||||
|
||||
if (reactions != null && reactions.length() > 0) {
|
||||
db.execute("DELETE FROM Reaction WHERE post_id = ?", arrayOf(post.getString("id")))
|
||||
insertReactions(db, reactions)
|
||||
}
|
||||
|
||||
if (customEmojis != null && customEmojis.length() > 0) {
|
||||
insertCustomEmojis(db, customEmojis)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun DatabaseHelper.handlePosts(db: Database, postsData: ReadableMap?, channelId: String, receivingThreads: Boolean) {
|
||||
// Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users
|
||||
try {
|
||||
if (postsData != null) {
|
||||
val ordered = postsData.getArray("order")?.toArrayList()
|
||||
val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap()
|
||||
val previousPostId = postsData.getString("prev_post_id")
|
||||
val postsInThread = hashMapOf<String, List<JSONObject>>()
|
||||
val postList = posts.toList()
|
||||
var earliest = 0.0
|
||||
var latest = 0.0
|
||||
|
||||
if (ordered != null && posts.isNotEmpty()) {
|
||||
val firstId = ordered.first()
|
||||
val lastId = ordered.last()
|
||||
var prevPostId = ""
|
||||
|
||||
val sortedPosts = postList.sortedBy { (_, value) ->
|
||||
((value as Map<*, *>)["create_at"] as Double)
|
||||
}
|
||||
|
||||
sortedPosts.forEachIndexed { index, it ->
|
||||
val key = it.first
|
||||
if (it.second != null) {
|
||||
@Suppress("UNCHECKED_CAST", "UNCHECKED_CAST")
|
||||
val post: MutableMap<String, Any?> = it.second as MutableMap<String, Any?>
|
||||
|
||||
if (index == 0) {
|
||||
post.putIfAbsent("prev_post_id", previousPostId)
|
||||
} else if (prevPostId.isNotEmpty()) {
|
||||
post.putIfAbsent("prev_post_id", prevPostId)
|
||||
}
|
||||
|
||||
if (lastId == key) {
|
||||
earliest = post["create_at"] as Double
|
||||
}
|
||||
if (firstId == key) {
|
||||
latest = post["create_at"] as Double
|
||||
}
|
||||
|
||||
val jsonPost = JSONObject(post)
|
||||
val postId = post["id"] as? String ?: ""
|
||||
val rootId = post["root_id"] as? String ?: ""
|
||||
val postInThread = rootId.ifEmpty { postId }
|
||||
var thread = postsInThread[postInThread]?.toMutableList()
|
||||
if (thread == null) {
|
||||
thread = mutableListOf()
|
||||
}
|
||||
|
||||
thread.add(jsonPost)
|
||||
postsInThread[postInThread] = thread.toList()
|
||||
|
||||
if (find(db, "Post", key) == null) {
|
||||
insertPost(db, jsonPost)
|
||||
} else {
|
||||
updatePost(db, jsonPost)
|
||||
}
|
||||
|
||||
if (ordered.contains(key)) {
|
||||
prevPostId = key
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!receivingThreads) {
|
||||
handlePostsInChannel(db, channelId, earliest, latest)
|
||||
}
|
||||
handlePostsInThread(db, postsInThread)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
internal fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? {
|
||||
for (i in 0 until chunks.size()) {
|
||||
val chunk = chunks.getMap(i)
|
||||
if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) {
|
||||
return chunk
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap? {
|
||||
return try {
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInChannel
|
||||
(id, channel_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, channelId, earliest, latest))
|
||||
|
||||
val map = Arguments.createMap()
|
||||
map.putString("id", id)
|
||||
map.putString("channel_id", channelId)
|
||||
map.putDouble("earliest", earliest)
|
||||
map.putDouble("latest", latest)
|
||||
map
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) {
|
||||
for (i in 0 until existingChunks.size()) {
|
||||
try {
|
||||
val chunk = existingChunks.getMap(i)
|
||||
if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") &&
|
||||
newChunk.getDouble("latest") >= chunk.getDouble("latest")) {
|
||||
db.execute("DELETE FROM PostsInChannel WHERE id = ?", arrayOf(chunk.getString("id")))
|
||||
break
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT id, channel_id, earliest, latest FROM PostsInChannel WHERE channel_id = ?",
|
||||
arrayOf(channelId)
|
||||
).use { cursor ->
|
||||
if (cursor.count == 0) {
|
||||
// create new post in channel
|
||||
insertPostInChannel(db, channelId, earliest, latest)
|
||||
return
|
||||
}
|
||||
|
||||
val resultArray = Arguments.createArray()
|
||||
while (cursor.moveToNext()) {
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
resultArray.pushMap(cursorMap)
|
||||
}
|
||||
|
||||
val chunk = findPostInChannel(resultArray, earliest, latest)
|
||||
if (chunk != null) {
|
||||
db.execute(
|
||||
"UPDATE PostsInChannel SET earliest = ?, latest = ?, _status = 'updated' WHERE id = ?",
|
||||
arrayOf(
|
||||
minOf(earliest, chunk.getDouble("earliest")),
|
||||
maxOf(latest, chunk.getDouble("latest")),
|
||||
chunk.getString("id")
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val newChunk = insertPostInChannel(db, channelId, earliest, latest)
|
||||
newChunk?.let { mergePostsInChannel(db, resultArray, it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun getTeammateDisplayNameSetting(db: Database): String {
|
||||
val configSetting = queryConfigDisplayNameSetting(db)
|
||||
if (configSetting != null) {
|
||||
return configSetting
|
||||
}
|
||||
|
||||
try {
|
||||
db.rawQuery(
|
||||
"SELECT value FROM Preference where category = ? AND name = ? limit 1",
|
||||
arrayOf("display_settings", "name_format")
|
||||
).use { cursor ->
|
||||
if (cursor.count <= 0) {
|
||||
return "username"
|
||||
}
|
||||
val resultMap = Arguments.createMap()
|
||||
cursor.moveToFirst()
|
||||
resultMap.mapCursor(cursor)
|
||||
return resultMap?.getString("value") ?: "username"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return "username"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONArray
|
||||
|
||||
internal fun insertReactions(db: Database, reactions: JSONArray) {
|
||||
for (i in 0 until reactions.length()) {
|
||||
try {
|
||||
val reaction = reactions.getJSONObject(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Reaction
|
||||
(id, create_at, emoji_name, post_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id,
|
||||
reaction.getDouble("create_at"), reaction.getString("emoji_name"),
|
||||
reaction.getString("post_id"), reaction.getString("user_id")
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.nozbe.watermelondb.Database
|
||||
import org.json.JSONObject
|
||||
|
||||
fun queryCurrentUserId(db: Database): String? {
|
||||
val result = find(db, "System", "currentUserId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryCurrentTeamId(db: Database): String? {
|
||||
val result = find(db, "System", "currentTeamId")
|
||||
return result?.getString("value")?.removeSurrounding("\"")
|
||||
}
|
||||
|
||||
fun queryConfigDisplayNameSetting(db: Database): String? {
|
||||
val license = find(db, "System", "license")
|
||||
val lockDisplayName = find(db, "Config", "LockTeammateNameDisplay")
|
||||
val displayName = find(db, "Config", "TeammateNameDisplay")
|
||||
|
||||
val licenseValue = license?.getString("value") ?: ""
|
||||
val lockDisplayNameValue = lockDisplayName?.getString("value") ?: "false"
|
||||
val displayNameValue = displayName?.getString("value") ?: "full_name"
|
||||
val licenseJson = JSONObject(licenseValue)
|
||||
val licenseLock = try { licenseJson.getString("LockTeammateNameDisplay") } catch (e: Exception) { "false"}
|
||||
|
||||
if (licenseLock == "true" && lockDisplayNameValue == "true") {
|
||||
return displayNameValue
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
|
||||
fun findTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "Team", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun findMyTeam(db: Database?, teamId: String): Boolean {
|
||||
if (db != null) {
|
||||
val team = find(db, "MyTeam", teamId)
|
||||
return team != null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun queryMyTeams(db: Database?): ArrayList<ReadableMap>? {
|
||||
db?.rawQuery("SELECT * FROM MyTeam")?.use { cursor ->
|
||||
val results = ArrayList<ReadableMap>()
|
||||
if (cursor.count > 0) {
|
||||
while(cursor.moveToNext()) {
|
||||
val map = Arguments.createMap()
|
||||
map.mapCursor(cursor)
|
||||
results.add(map)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun insertTeam(db: Database, team: ReadableMap): Boolean {
|
||||
val id = try { team.getString("id") } catch (e: Exception) { return false }
|
||||
val deleteAt = try {team.getDouble("delete_at") } catch (e: Exception) { 0 }
|
||||
if (deleteAt.toInt() > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val isAllowOpenInvite = try { team.getBoolean("allow_open_invite") } catch (e: NoSuchKeyException) { false }
|
||||
val description = try { team.getString("description") } catch (e: NoSuchKeyException) { "" }
|
||||
val displayName = try { team.getString("display_name") } catch (e: NoSuchKeyException) { "" }
|
||||
val name = try { team.getString("name") } catch (e: NoSuchKeyException) { "" }
|
||||
val updateAt = try { team.getDouble("update_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val type = try { team.getString("type") } catch (e: NoSuchKeyException) { "O" }
|
||||
val allowedDomains = try { team.getString("allowed_domains") } catch (e: NoSuchKeyException) { "" }
|
||||
val isGroupConstrained = try { team.getBoolean("group_constrained") } catch (e: NoSuchKeyException) { false }
|
||||
val lastTeamIconUpdatedAt = try { team.getDouble("last_team_icon_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
val inviteId = try { team.getString("invite_id") } catch (e: NoSuchKeyException) { "" }
|
||||
val status = "created"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Team (
|
||||
id, allow_open_invite, description, display_name, name, update_at, type, allowed_domains,
|
||||
group_constrained, last_team_icon_update, invite_id, _changed, _status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, isAllowOpenInvite, description, displayName, name, updateAt,
|
||||
type, allowedDomains, isGroupConstrained, lastTeamIconUpdatedAt, inviteId, status
|
||||
)
|
||||
)
|
||||
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun insertMyTeam(db: Database, myTeam: ReadableMap): Boolean {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return false
|
||||
val id = try { myTeam.getString("id") } catch (e: NoSuchKeyException) { return false }
|
||||
val roles = try { myTeam.getString("roles") } catch (e: NoSuchKeyException) { "" }
|
||||
val schemeAdmin = try { myTeam.getBoolean("scheme_admin") } catch (e: NoSuchKeyException) { false }
|
||||
val status = "created"
|
||||
val membershipId = "$id-$currentUserId"
|
||||
|
||||
return try {
|
||||
db.execute(
|
||||
"INSERT INTO MyTeam (id, roles, _changed, _status) VALUES (?, ?, '', ?)",
|
||||
arrayOf(id, roles, status)
|
||||
)
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO TeamMembership (id, team_id, user_id, scheme_admin, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', ?)
|
||||
""".trimIndent(),
|
||||
arrayOf(membershipId, id, currentUserId, schemeAdmin, status)
|
||||
)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.RandomId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import com.nozbe.watermelondb.mapCursor
|
||||
import org.json.JSONObject
|
||||
|
||||
internal fun insertThread(db: Database, thread: ReadableMap) {
|
||||
// These fields are not present when we extract threads from posts
|
||||
try {
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { false }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { 0 }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { 0 }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO Thread
|
||||
(id, last_reply_at, last_fetched_at, last_viewed_at, reply_count, is_following, unread_replies, unread_mentions, viewed_at, _changed, _status)
|
||||
VALUES (?, ?, 0, ?, ?, ?, ?, ?, 0, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
id, lastReplyAt, lastViewedAt,
|
||||
replyCount, isFollowing, unreadReplies, unreadMentions
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateThread(db: Database, thread: ReadableMap, existingRecord: ReadableMap) {
|
||||
try {
|
||||
// These fields are not present when we extract threads from posts
|
||||
val id = try { thread.getString("id") } catch (e: NoSuchKeyException) { return }
|
||||
val isFollowing = try { thread.getBoolean("is_following") } catch (e: NoSuchKeyException) { existingRecord.getInt("is_following") == 1 }
|
||||
val lastViewedAt = try { thread.getDouble("last_viewed_at") } catch (e: NoSuchKeyException) { existingRecord.getDouble("last_viewed_at") }
|
||||
val unreadReplies = try { thread.getInt("unread_replies") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_replies") }
|
||||
val unreadMentions = try { thread.getInt("unread_mentions") } catch (e: NoSuchKeyException) { existingRecord.getInt("unread_mentions") }
|
||||
val lastReplyAt = try { thread.getDouble("last_reply_at") } catch (e: NoSuchKeyException) { 0 }
|
||||
val replyCount = try { thread.getInt("reply_count") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
db.execute(
|
||||
"""
|
||||
UPDATE Thread SET
|
||||
last_reply_at = ?, last_viewed_at = ?, reply_count = ?, is_following = ?, unread_replies = ?,
|
||||
unread_mentions = ?, _status = 'updated' where id = ?
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
lastReplyAt, lastViewedAt, replyCount,
|
||||
isFollowing, unreadReplies, unreadMentions, id
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun insertThreadParticipants(db: Database, threadId: String, participants: ReadableArray) {
|
||||
for (i in 0 until participants.size()) {
|
||||
try {
|
||||
val participant = participants.getMap(i)
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO ThreadParticipant
|
||||
(id, thread_id, user_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, threadId, participant.getString("id"))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double) {
|
||||
try {
|
||||
val query = """
|
||||
INSERT INTO TeamThreadsSync (id, _changed, _status, earliest, latest)
|
||||
VALUES (?, '', 'created', ?, ?)
|
||||
"""
|
||||
db.execute(query, arrayOf(teamId, earliest, latest))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTeamThreadsSync(db: Database, teamId: String, earliest: Double, latest: Double, existingRecord: ReadableMap) {
|
||||
try {
|
||||
val storeEarliest = minOf(earliest, existingRecord.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, existingRecord.getDouble("latest"))
|
||||
val query = "UPDATE TeamThreadsSync SET earliest=?, latest=? WHERE id=?"
|
||||
db.execute(query, arrayOf(storeEarliest, storeLatest, teamId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun syncParticipants(db: Database, thread: ReadableMap) {
|
||||
try {
|
||||
val threadId = thread.getString("id")
|
||||
val participants = thread.getArray("participants")
|
||||
if (participants != null) {
|
||||
db.execute("DELETE FROM ThreadParticipant WHERE thread_id = ?", arrayOf(threadId))
|
||||
|
||||
if (participants.size() > 0) {
|
||||
insertThreadParticipants(db, threadId!!, participants)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun handlePostsInThread(db: Database, postsInThread: Map<String, List<JSONObject>>) {
|
||||
postsInThread.forEach { (key, list) ->
|
||||
try {
|
||||
val sorted = list.sortedBy { it.getDouble("create_at") }
|
||||
val earliest = sorted.first().getDouble("create_at")
|
||||
val latest = sorted.last().getDouble("create_at")
|
||||
db.rawQuery("SELECT * FROM PostsInThread WHERE root_id = ? ORDER BY latest DESC", arrayOf(key)).use { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
cursor.moveToFirst()
|
||||
val cursorMap = Arguments.createMap()
|
||||
cursorMap.mapCursor(cursor)
|
||||
val storeEarliest = minOf(earliest, cursorMap.getDouble("earliest"))
|
||||
val storeLatest = maxOf(latest, cursorMap.getDouble("latest"))
|
||||
db.execute(
|
||||
"UPDATE PostsInThread SET earliest = ?, latest = ?, _status = 'updated' WHERE root_id = ?",
|
||||
arrayOf(
|
||||
storeEarliest,
|
||||
storeLatest,
|
||||
key
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val id = RandomId.generate()
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO PostsInThread
|
||||
(id, root_id, earliest, latest, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(id, key, earliest, latest)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreads(db: Database, threads: ArrayList<ReadableMap>, teamId: String?) {
|
||||
val teamIds = ArrayList<String>()
|
||||
if (teamId.isNullOrEmpty()) {
|
||||
val myTeams = queryMyTeams(db)
|
||||
if (myTeams != null) {
|
||||
for (myTeam in myTeams) {
|
||||
myTeam.getString("id")?.let { teamIds.add(it) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
teamIds.add(teamId)
|
||||
}
|
||||
|
||||
for (i in 0 until threads.size) {
|
||||
try {
|
||||
val thread = threads[i]
|
||||
handleThread(db, thread, teamIds)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
handleTeamThreadsSync(db, threads, teamIds)
|
||||
}
|
||||
|
||||
fun handleThread(db: Database, thread: ReadableMap, teamIds: ArrayList<String>) {
|
||||
// Insert/Update the thread
|
||||
val threadId = thread.getString("id")
|
||||
val isFollowing = thread.getBoolean("is_following")
|
||||
val existingRecord = find(db, "Thread", threadId)
|
||||
if (existingRecord == null) {
|
||||
insertThread(db, thread)
|
||||
} else {
|
||||
updateThread(db, thread, existingRecord)
|
||||
}
|
||||
|
||||
syncParticipants(db, thread)
|
||||
|
||||
// this is per team
|
||||
if (isFollowing) {
|
||||
for (teamId in teamIds) {
|
||||
handleThreadInTeam(db, thread, teamId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleThreadInTeam(db: Database, thread: ReadableMap, teamId: String) {
|
||||
val threadId = thread.getString("id") ?: return
|
||||
val existingRecord = findByColumns(
|
||||
db,
|
||||
"ThreadsInTeam",
|
||||
arrayOf("thread_id", "team_id"),
|
||||
arrayOf(threadId, teamId)
|
||||
)
|
||||
if (existingRecord == null) {
|
||||
try {
|
||||
val id = RandomId.generate()
|
||||
val query = """
|
||||
INSERT INTO ThreadsInTeam (id, team_id, thread_id, _changed, _status)
|
||||
VALUES (?, ?, ?, '', 'created')
|
||||
"""
|
||||
db.execute(query, arrayOf(id, teamId, threadId))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTeamThreadsSync(db: Database, threadList: ArrayList<ReadableMap>, teamIds: ArrayList<String>) {
|
||||
val sortedList = threadList.filter{ it.getBoolean("is_following") }
|
||||
.sortedBy { it.getDouble("last_reply_at") }
|
||||
.map { it.getDouble("last_reply_at") }
|
||||
val earliest = sortedList.first()
|
||||
val latest = sortedList.last()
|
||||
|
||||
for (teamId in teamIds) {
|
||||
val existingTeamThreadsSync = find(db, "TeamThreadsSync", teamId)
|
||||
if (existingTeamThreadsSync == null) {
|
||||
insertTeamThreadsSync(db, teamId, earliest, latest)
|
||||
} else {
|
||||
updateTeamThreadsSync(db, teamId, earliest, latest, existingTeamThreadsSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
fun getLastPictureUpdate(db: Database?, userId: String): Double? {
|
||||
try {
|
||||
if (db != null) {
|
||||
var id = userId
|
||||
if (userId == "me") {
|
||||
(queryCurrentUserId(db) ?: userId).also { id = it }
|
||||
}
|
||||
val userQuery = "SELECT last_picture_update FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(id)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getDouble(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentUserLocale(db: Database): String {
|
||||
try {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return "en"
|
||||
val userQuery = "SELECT locale FROM User WHERE id=?"
|
||||
db.rawQuery(userQuery, arrayOf(currentUserId)).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return "en"
|
||||
}
|
||||
|
||||
fun handleUsers(db: Database, users: ReadableArray) {
|
||||
for (i in 0 until users.size()) {
|
||||
val user = users.getMap(i)
|
||||
val roles = user.getString("roles") ?: ""
|
||||
val isBot = try {
|
||||
user.getBoolean("is_bot")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
}
|
||||
|
||||
val lastPictureUpdate = try { user.getDouble("last_picture_update") } catch (e: NoSuchKeyException) { 0 }
|
||||
|
||||
try {
|
||||
db.execute(
|
||||
"""
|
||||
INSERT INTO User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest,
|
||||
last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props,
|
||||
props, timezone, _changed, _status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'created')
|
||||
""".trimIndent(),
|
||||
arrayOf(
|
||||
user.getString("id"),
|
||||
user.getString("auth_service"), user.getDouble("update_at"), user.getDouble("delete_at"),
|
||||
user.getString("email"), user.getString("first_name"), isBot,
|
||||
roles.contains("system_guest"), user.getString("last_name"), lastPictureUpdate,
|
||||
user.getString("locale"), user.getString("nickname"), user.getString("position"),
|
||||
roles, "", user.getString("username"), "{}",
|
||||
ReadableMapUtils.toJSONObject(user.getMap("props")
|
||||
?: Arguments.createMap()).toString(),
|
||||
ReadableMapUtils.toJSONObject(user.getMap("timezone")
|
||||
?: Arguments.createMap()).toString(),
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findByColumns
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.mattermost.helpers.database_extension.queryMyTeams
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyTeamCategories(db: Database, serverUrl: String, teamId: String): ReadableMap? {
|
||||
return try {
|
||||
val userId = queryCurrentUserId(db)
|
||||
val categories = fetch(serverUrl, "/api/v4/users/$userId/teams/$teamId/channels/categories")
|
||||
categories?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun PushNotificationDataRunnable.Companion.addToDefaultCategoryIfNeeded(db: Database, channel: ReadableMap): ReadableArray? {
|
||||
val channelId = channel.getString("id") ?: return null
|
||||
val channelType = channel.getString("type")
|
||||
val categoryChannels = Arguments.createArray()
|
||||
if (channelType == "D" || channelType == "G") {
|
||||
val myTeams = queryMyTeams(db)
|
||||
myTeams?.let {
|
||||
for (myTeam in it) {
|
||||
val map = categoryChannelForTeam(db, channelId, myTeam.getString("id"), "direct_messages")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val map = categoryChannelForTeam(db, channelId, channel.getString("team_id"), "channels")
|
||||
if (map != null) {
|
||||
categoryChannels.pushMap(map)
|
||||
}
|
||||
}
|
||||
|
||||
return categoryChannels
|
||||
}
|
||||
|
||||
private fun categoryChannelForTeam(db: Database, channelId: String, teamId: String?, type: String): ReadableMap? {
|
||||
teamId?.let { id ->
|
||||
val category = findByColumns(db, "Category", arrayOf("type", "team_id"), arrayOf(type, id))
|
||||
val categoryId = category?.getString("id")
|
||||
categoryId?.let { cId ->
|
||||
val cc = findByColumns(
|
||||
db,
|
||||
"CategoryChannel",
|
||||
arrayOf("category_id", "channel_id"),
|
||||
arrayOf(cId, channelId)
|
||||
)
|
||||
if (cc == null) {
|
||||
val map = Arguments.createMap()
|
||||
map.putString("channel_id", channelId)
|
||||
map.putString("category_id", cId)
|
||||
map.putString("id", "${id}_$channelId")
|
||||
return map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findChannel
|
||||
import com.mattermost.helpers.database_extension.getCurrentUserLocale
|
||||
import com.mattermost.helpers.database_extension.getTeammateDisplayNameSetting
|
||||
import com.mattermost.helpers.database_extension.queryCurrentUserId
|
||||
import com.nozbe.watermelondb.Database
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchMyChannel(db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean): Triple<ReadableMap?, ReadableMap?, ReadableArray?> {
|
||||
val channel = fetch(serverUrl, "/api/v4/channels/$channelId")
|
||||
var channelData = channel?.getMap("data")
|
||||
val myChannelData = channelData?.let { fetchMyChannelData(serverUrl, channelId, isCRTEnabled, it) }
|
||||
val channelType = channelData?.getString("type")
|
||||
var profilesArray: ReadableArray? = null
|
||||
|
||||
if (channelData != null && channelType != null && !findChannel(db, channelId)) {
|
||||
val displayNameSetting = getTeammateDisplayNameSetting(db)
|
||||
|
||||
when (channelType) {
|
||||
"D" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val displayName = displayUsername(profilesArray!!.getMap(0), displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
"G" -> {
|
||||
profilesArray = fetchProfileInChannel(db, serverUrl, channelId)
|
||||
if ((profilesArray?.size() ?: 0) > 0) {
|
||||
val localeString = getCurrentUserLocale(db)
|
||||
val localeArray = localeString.split("-")
|
||||
val locale = if (localeArray.size == 1) {
|
||||
Locale(localeString)
|
||||
} else {
|
||||
Locale(localeArray[0], localeArray[1])
|
||||
}
|
||||
val displayName = displayGroupMessageName(profilesArray!!, locale, displayNameSetting)
|
||||
val data = Arguments.createMap()
|
||||
data.merge(channelData)
|
||||
data.putString("display_name", displayName)
|
||||
channelData = data
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(channelData, myChannelData, profilesArray)
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchMyChannelData(serverUrl: String, channelId: String, isCRTEnabled: Boolean, channelData: ReadableMap): ReadableMap? {
|
||||
try {
|
||||
val myChannel = fetch(serverUrl, "/api/v4/channels/$channelId/members/me")
|
||||
val myChannelData = myChannel?.getMap("data")
|
||||
if (myChannelData != null) {
|
||||
val data = Arguments.createMap()
|
||||
data.merge(myChannelData)
|
||||
data.putString("id", channelId)
|
||||
|
||||
val totalMsg = if (isCRTEnabled) {
|
||||
channelData.getInt("total_msg_count_root")
|
||||
} else {
|
||||
channelData.getInt("total_msg_count")
|
||||
}
|
||||
|
||||
val myMsgCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("msg_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("msg_count")
|
||||
}
|
||||
|
||||
val mentionCount = if (isCRTEnabled) {
|
||||
myChannelData.getInt("mention_count_root")
|
||||
} else {
|
||||
myChannelData.getInt("mention_count")
|
||||
}
|
||||
|
||||
val lastPostAt = if (isCRTEnabled) {
|
||||
try {
|
||||
channelData.getDouble("last_root_post_at")
|
||||
} catch (e: Exception) {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
} else {
|
||||
channelData.getDouble("last_post_at")
|
||||
}
|
||||
|
||||
val messageCount = 0.coerceAtLeast(totalMsg - myMsgCount)
|
||||
data.putInt("message_count", messageCount)
|
||||
data.putInt("mentions_count", mentionCount)
|
||||
data.putBoolean("is_unread", messageCount > 0)
|
||||
data.putDouble("last_post_at", lastPostAt)
|
||||
return data
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun PushNotificationDataRunnable.Companion.fetchProfileInChannel(db: Database, serverUrl: String, channelId: String): ReadableArray? {
|
||||
return try {
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val profilesInChannel = fetch(serverUrl, "/api/v4/users?in_channel=${channelId}&page=0&per_page=8&sort=")
|
||||
val profilesArray = profilesInChannel?.getArray("data")
|
||||
val result = Arguments.createArray()
|
||||
if (profilesArray != null) {
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
if (profile.getString("id") != currentUserId) {
|
||||
result.pushMap(profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayUsername(user: ReadableMap, displayNameSetting: String): String {
|
||||
val name = user.getString("username") ?: ""
|
||||
val nickname = user.getString("nickname")
|
||||
val firstName = user.getString("first_name") ?: ""
|
||||
val lastName = user.getString("last_name") ?: ""
|
||||
return when (displayNameSetting) {
|
||||
"nickname_full_name" -> {
|
||||
(nickname ?: "$firstName $lastName").trim()
|
||||
}
|
||||
"full_name" -> {
|
||||
"$firstName $lastName".trim()
|
||||
}
|
||||
else -> {
|
||||
name.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PushNotificationDataRunnable.Companion.displayGroupMessageName(profilesArray: ReadableArray, locale: Locale, displayNameSetting: String): String {
|
||||
val names = ArrayList<String>()
|
||||
for (i in 0 until profilesArray.size()) {
|
||||
val profile = profilesArray.getMap(i)
|
||||
names.add(displayUsername(profile, displayNameSetting))
|
||||
}
|
||||
|
||||
return names.sortedWith { s1, s2 ->
|
||||
Collator.getInstance(locale).compare(s1, s2)
|
||||
}.joinToString(", ").trim()
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.Network
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ResolvePromise
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetch(serverUrl: String, endpoint: String): ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.get(serverUrl, endpoint, null, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
if (response != null && !response.getBoolean("ok")) {
|
||||
val error = response.getMap("data")
|
||||
cont.resumeWith(Result.failure((IOException("Unexpected code ${error?.getInt("status_code")} ${error?.getString("message")}"))))
|
||||
} else {
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchWithPost(serverUrl: String, endpoint: String, options: ReadableMap?) : ReadableMap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Network.post(serverUrl, endpoint, options, object : ResolvePromise() {
|
||||
override fun resolve(value: Any?) {
|
||||
val response = value as ReadableMap?
|
||||
cont.resumeWith(Result.success(response))
|
||||
}
|
||||
|
||||
override fun reject(code: String, message: String) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $code $message")))
|
||||
}
|
||||
|
||||
override fun reject(reason: Throwable?) {
|
||||
cont.resumeWith(Result.failure(IOException("Unexpected code $reason")))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.NoSuchKeyException
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.WritableNativeArray
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
import com.mattermost.helpers.ReadableMapUtils
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchPosts(
|
||||
db: Database, serverUrl: String, channelId: String, isCRTEnabled: Boolean,
|
||||
rootId: String?, loadedProfiles: ReadableArray?
|
||||
): ReadableMap? {
|
||||
return try {
|
||||
val regex = Regex("""\B@(([a-z\d-._]*[a-z\d_])[.-]*)""", setOf(RegexOption.IGNORE_CASE))
|
||||
val currentUserId = queryCurrentUserId(db)
|
||||
val currentUser = find(db, "User", currentUserId)
|
||||
val currentUsername = currentUser?.getString("username")
|
||||
|
||||
var additionalParams = ""
|
||||
if (isCRTEnabled) {
|
||||
additionalParams = "&collapsedThreads=true&collapsedThreadsExtended=true"
|
||||
}
|
||||
|
||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||
val endpoint = if (receivingThreads) {
|
||||
val since = rootId?.let { queryLastPostInThread(db, it) }
|
||||
val queryParams = if (since == null) "?perPage=60&fromCreatedAt=0&direction=up" else
|
||||
"?fromCreateAt=${since.toLong()}&direction=down"
|
||||
|
||||
"/api/v4/posts/$rootId/thread$queryParams$additionalParams"
|
||||
} else {
|
||||
val since = queryPostSinceForChannel(db, channelId)
|
||||
val queryParams = if (since == null) "?page=0&per_page=60" else "?since=${since.toLong()}"
|
||||
"/api/v4/channels/$channelId/posts$queryParams$additionalParams"
|
||||
}
|
||||
|
||||
val postsResponse = fetch(serverUrl, endpoint)
|
||||
val postData = postsResponse?.getMap("data")
|
||||
val results = Arguments.createMap()
|
||||
|
||||
if (postData != null) {
|
||||
val data = ReadableMapUtils.toMap(postData)
|
||||
results.putMap("posts", postData)
|
||||
if (data != null) {
|
||||
val postsMap = data["posts"]
|
||||
if (postsMap != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val posts = ReadableMapUtils.toWritableMap(postsMap as? Map<String, Any>)
|
||||
val iterator = posts.keySetIterator()
|
||||
val userIds = mutableListOf<String>()
|
||||
val usernames = mutableListOf<String>()
|
||||
|
||||
val threads = WritableNativeArray()
|
||||
val threadParticipantUserIds = mutableListOf<String>() // Used to exclude the "userIds" present in the thread participants
|
||||
val threadParticipantUsernames = mutableListOf<String>() // Used to exclude the "usernames" present in the thread participants
|
||||
val threadParticipantUsers = HashMap<String, ReadableMap>() // All unique users from thread participants are stored here
|
||||
val userIdsAlreadyLoaded = mutableListOf<String>()
|
||||
if (loadedProfiles != null) {
|
||||
for (i in 0 until loadedProfiles.size()) {
|
||||
loadedProfiles.getMap(i).getString("id")?.let { userIdsAlreadyLoaded.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
while (iterator.hasNextKey()) {
|
||||
val key = iterator.nextKey()
|
||||
val post = posts.getMap(key)
|
||||
val userId = post?.getString("user_id")
|
||||
if (userId != null && userId != currentUserId && !userIdsAlreadyLoaded.contains(userId) && !userIds.contains(userId)) {
|
||||
userIds.add(userId)
|
||||
}
|
||||
val message = post?.getString("message")
|
||||
if (message != null) {
|
||||
val matchResults = regex.findAll(message)
|
||||
matchResults.iterator().forEach {
|
||||
val username = it.value.removePrefix("@")
|
||||
if (!usernames.contains(username) && currentUsername != username && !specialMentions.contains(username)) {
|
||||
usernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isCRTEnabled) {
|
||||
// Add root post as a thread
|
||||
val threadId = post?.getString("root_id")
|
||||
if (threadId.isNullOrEmpty()) {
|
||||
post?.let {
|
||||
val thread = Arguments.createMap()
|
||||
thread.putString("id", it.getString("id"))
|
||||
thread.putInt("reply_count", it.getInt("reply_count"))
|
||||
thread.putDouble("last_reply_at", 0.0)
|
||||
thread.putDouble("last_viewed_at", 0.0)
|
||||
thread.putArray("participants", it.getArray("participants"))
|
||||
thread.putMap("post", it)
|
||||
thread.putBoolean("is_following", try {
|
||||
it.getBoolean("is_following")
|
||||
} catch (e: NoSuchKeyException) {
|
||||
false
|
||||
})
|
||||
thread.putInt("unread_replies", 0)
|
||||
thread.putInt("unread_mentions", 0)
|
||||
thread.putDouble("delete_at", it.getDouble("delete_at"))
|
||||
threads.pushMap(thread)
|
||||
}
|
||||
}
|
||||
|
||||
// Add participant userIds and usernames to exclude them from getting fetched again
|
||||
val participants = post?.getArray("participants")
|
||||
participants?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
val participant = it.getMap(i)
|
||||
|
||||
val participantId = participant.getString("id")
|
||||
if (participantId != currentUserId && participantId != null) {
|
||||
if (!threadParticipantUserIds.contains(participantId) && !userIdsAlreadyLoaded.contains(participantId)) {
|
||||
threadParticipantUserIds.add(participantId)
|
||||
}
|
||||
|
||||
if (!threadParticipantUsers.containsKey(participantId)) {
|
||||
threadParticipantUsers[participantId] = participant
|
||||
}
|
||||
}
|
||||
|
||||
val username = participant.getString("username")
|
||||
if (username != null && username != currentUsername && !threadParticipantUsernames.contains(username)) {
|
||||
threadParticipantUsernames.add(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val existingUserIds = queryIds(db, "User", userIds.toTypedArray())
|
||||
val existingUsernames = queryByColumn(db, "User", "username", usernames.toTypedArray())
|
||||
userIds.removeAll { it in existingUserIds }
|
||||
usernames.removeAll { it in existingUsernames }
|
||||
|
||||
if (threadParticipantUserIds.size > 0) {
|
||||
// Do not fetch users found in thread participants as we get the user's data in the posts response already
|
||||
userIds.removeAll { it in threadParticipantUserIds }
|
||||
usernames.removeAll { it in threadParticipantUsernames }
|
||||
|
||||
// Get users from thread participants
|
||||
val existingThreadParticipantUserIds = queryIds(db, "User", threadParticipantUserIds.toTypedArray())
|
||||
|
||||
// Exclude the thread participants already present in the DB from getting inserted again
|
||||
val usersFromThreads = WritableNativeArray()
|
||||
threadParticipantUsers.forEach { (userId, user) ->
|
||||
if (!existingThreadParticipantUserIds.contains(userId)) {
|
||||
usersFromThreads.pushMap(user)
|
||||
}
|
||||
}
|
||||
|
||||
if (usersFromThreads.size() > 0) {
|
||||
results.putArray("usersFromThreads", usersFromThreads)
|
||||
}
|
||||
}
|
||||
|
||||
if (userIds.size > 0) {
|
||||
results.putArray("userIdsToLoad", ReadableArrayUtils.toWritableArray(userIds.toTypedArray()))
|
||||
}
|
||||
|
||||
if (usernames.size > 0) {
|
||||
results.putArray("usernamesToLoad", ReadableArrayUtils.toWritableArray(usernames.toTypedArray()))
|
||||
}
|
||||
|
||||
if (threads.size() > 0) {
|
||||
results.putArray("threads", threads)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.findMyTeam
|
||||
import com.mattermost.helpers.database_extension.findTeam
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
suspend fun PushNotificationDataRunnable.Companion.fetchTeamIfNeeded(db: Database, serverUrl: String, teamId: String): Pair<ReadableMap?, ReadableMap?> {
|
||||
return try {
|
||||
var team: ReadableMap? = null
|
||||
var myTeam: ReadableMap? = null
|
||||
val teamExists = findTeam(db, teamId)
|
||||
val myTeamExists = findMyTeam(db, teamId)
|
||||
if (!teamExists) {
|
||||
team = fetch(serverUrl, "/api/v4/teams/$teamId")
|
||||
}
|
||||
|
||||
if (!myTeamExists) {
|
||||
myTeam = fetch(serverUrl, "/api/v4/teams/$teamId/members/me")
|
||||
}
|
||||
|
||||
Pair(team, myTeam)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Pair(null, null)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.database_extension.*
|
||||
import com.nozbe.watermelondb.Database
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchThread(db: Database, serverUrl: String, threadId: String, teamId: String?): ReadableMap? {
|
||||
val currentUserId = queryCurrentUserId(db) ?: return null
|
||||
val threadTeamId = (if (teamId.isNullOrEmpty()) queryCurrentTeamId(db) else teamId) ?: return null
|
||||
|
||||
return try {
|
||||
val thread = fetch(serverUrl, "/api/v4/users/$currentUserId/teams/${threadTeamId}/threads/$threadId")
|
||||
thread?.getMap("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.mattermost.helpers.push_notification
|
||||
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.mattermost.helpers.PushNotificationDataRunnable
|
||||
import com.mattermost.helpers.ReadableArrayUtils
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersById(serverUrl: String, userIds: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/ids"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(userIds)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchUsersByUsernames(serverUrl: String, usernames: ReadableArray): ReadableArray? {
|
||||
return try {
|
||||
val endpoint = "api/v4/users/usernames"
|
||||
val options = Arguments.createMap()
|
||||
options.putArray("body", ReadableArrayUtils.toWritableArray(ReadableArrayUtils.toArray(usernames)))
|
||||
val result = fetchWithPost(serverUrl, endpoint, options)
|
||||
result?.getArray("data")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PushNotificationDataRunnable.Companion.fetchNeededUsers(serverUrl: String, loadedUsers: ReadableArray?, data: ReadableMap?): ArrayList<Any> {
|
||||
val userList = ArrayList<Any>()
|
||||
loadedUsers?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
data?.getArray("userIdsToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersById(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usernamesToLoad")?.let { ids ->
|
||||
if (ids.size() > 0) {
|
||||
val result = fetchUsersByUsernames(serverUrl, ids)
|
||||
result?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
}
|
||||
}
|
||||
|
||||
data?.getArray("usersFromThreads")?.let { PushNotificationDataRunnable.addUsersToList(it, userList) }
|
||||
|
||||
return userList
|
||||
}
|
||||
|
||||
internal fun PushNotificationDataRunnable.Companion.addUsersToList(users: ReadableArray, list: ArrayList<Any>) {
|
||||
for (i in 0 until users.size()) {
|
||||
list.add(users.getMap(i))
|
||||
}
|
||||
}
|
||||
@@ -7,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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ buildscript {
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
supportLibVersion = "33.0.0"
|
||||
kotlinVersion = "1.7.21"
|
||||
kotlin_version = "1.7.21"
|
||||
firebaseVersion = "23.1.1"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlin_version = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
@@ -23,7 +23,7 @@ buildscript {
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.3.1")
|
||||
classpath("com.facebook.react:react-native-gradle-plugin")
|
||||
classpath('com.google.gms:google-services:4.3.15')
|
||||
classpath('com.google.gms:google-services:4.3.14')
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
||||
@@ -29,7 +29,7 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.177.0
|
||||
FLIPPER_VERSION=0.125.0
|
||||
|
||||
# Use this property to specify which architecture you want to build.
|
||||
# You can also override it from the CLI using
|
||||
|
||||
@@ -5,8 +5,6 @@ project(':reactnativenotifications').projectDir = new File(rootProject.projectDi
|
||||
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
|
||||
include ':react-native-video'
|
||||
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android-exoplayer')
|
||||
include ':watermelondb'
|
||||
project(':watermelondb').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android')
|
||||
include ':watermelondb-jsi'
|
||||
project(':watermelondb-jsi').projectDir = new File(rootProject.projectDir, '../node_modules/@nozbe/watermelondb/native/android-jsi')
|
||||
includeBuild('../node_modules/react-native-gradle-plugin')
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
58
app/actions/local/notification.ts
Normal file
58
app/actions/local/notification.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPostById, queryPostsInChannel, queryPostsInThread} from '@queries/servers/post';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
export const updatePostSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
if (notification.payload?.channel_id) {
|
||||
const chunks = await queryPostsInChannel(database, notification.payload.channel_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
logError('Failed updatePostSinceCache', error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePostsInThreadsSinceCache = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
if (notification.payload?.root_id) {
|
||||
const {database} = operator;
|
||||
const chunks = await queryPostsInThread(database, notification.payload.root_id).fetch();
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
const lastPost = await getPostById(database, notification.payload.post_id);
|
||||
if (lastPost) {
|
||||
await operator.database.write(async () => {
|
||||
await recent.update(() => {
|
||||
recent.latest = lastPost.createAt;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user