Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2faccb7f | ||
|
|
5e3c6eec8f | ||
|
|
0c5ca33a10 | ||
|
|
765ee984ac | ||
|
|
91910693af | ||
|
|
c9314b2315 | ||
|
|
d0aa643e94 | ||
|
|
230efe1eb9 | ||
|
|
444ca1c7b5 | ||
|
|
4d9b298187 |
30 changed files with 479 additions and 249 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Mattermost Mobile v2
|
# Mattermost Mobile v2
|
||||||
|
|
||||||
- **Minimum Server versions:** Current ESR version (8.1.0+)
|
- **Minimum Server versions:** Current ESR version (9.5.0+)
|
||||||
- **Supported iOS versions:** 12.4+
|
- **Supported iOS versions:** 12.4+
|
||||||
- **Supported Android versions:** 7.0+
|
- **Supported Android versions:** 7.0+
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,8 +111,8 @@ android {
|
||||||
applicationId "com.mattermost.rnbeta"
|
applicationId "com.mattermost.rnbeta"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 520
|
versionCode 529
|
||||||
versionName "2.16.0"
|
versionName "2.17.1"
|
||||||
testBuildType System.getProperty('testBuildType', 'debug')
|
testBuildType System.getProperty('testBuildType', 'debug')
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,27 @@ import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
|
|
||||||
import com.facebook.react.bridge.ReadableArray
|
import com.facebook.react.bridge.ReadableArray
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
import com.mattermost.helpers.database_extension.getDatabaseForServer
|
||||||
import com.mattermost.helpers.database_extension.*
|
import com.mattermost.helpers.database_extension.saveToDatabase
|
||||||
import com.mattermost.helpers.push_notification.*
|
import com.mattermost.helpers.push_notification.addToDefaultCategoryIfNeeded
|
||||||
|
import com.mattermost.helpers.push_notification.fetchMyChannel
|
||||||
import kotlinx.coroutines.*
|
import com.mattermost.helpers.push_notification.fetchMyTeamCategories
|
||||||
|
import com.mattermost.helpers.push_notification.fetchNeededUsers
|
||||||
|
import com.mattermost.helpers.push_notification.fetchPosts
|
||||||
|
import com.mattermost.helpers.push_notification.fetchTeamIfNeeded
|
||||||
|
import com.mattermost.helpers.push_notification.fetchThread
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class PushNotificationDataHelper(private val context: Context) {
|
class PushNotificationDataHelper(private val context: Context) {
|
||||||
private var coroutineScope = CoroutineScope(Dispatchers.Default)
|
suspend fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||||
fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
return withContext(Dispatchers.Default) {
|
||||||
var result: Bundle? = null
|
PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||||
val job = coroutineScope.launch(Dispatchers.Default) {
|
|
||||||
result = PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
|
||||||
}
|
}
|
||||||
runBlocking {
|
|
||||||
job.join()
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,8 +35,8 @@ class PushNotificationDataRunnable {
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
suspend fun start(context: Context, initialData: Bundle, isReactInit: Boolean): Bundle? {
|
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 {
|
mutex.withLock {
|
||||||
|
// for more info see: https://blog.danlew.net/2020/01/28/coroutines-and-java-synchronization-dont-mix/
|
||||||
val serverUrl: String = initialData.getString("server_url") ?: return null
|
val serverUrl: String = initialData.getString("server_url") ?: return null
|
||||||
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
val db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||||
var result: Bundle? = null
|
var result: Bundle? = null
|
||||||
|
|
@ -50,8 +48,9 @@ class PushNotificationDataRunnable {
|
||||||
val postId = initialData.getString("post_id")
|
val postId = initialData.getString("post_id")
|
||||||
val rootId = initialData.getString("root_id")
|
val rootId = initialData.getString("root_id")
|
||||||
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
val isCRTEnabled = initialData.getString("is_crt_enabled") == "true"
|
||||||
|
val ackId = initialData.getString("ack_id")
|
||||||
|
|
||||||
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId")
|
Log.i("ReactNative", "Start fetching notification data in server=$serverUrl for channel=$channelId and ack=$ackId")
|
||||||
|
|
||||||
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
val receivingThreads = isCRTEnabled && !rootId.isNullOrEmpty()
|
||||||
val notificationData = Arguments.createMap()
|
val notificationData = Arguments.createMap()
|
||||||
|
|
@ -89,7 +88,7 @@ class PushNotificationDataRunnable {
|
||||||
|
|
||||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||||
val threadsArray = Arguments.createArray()
|
val threadsArray = Arguments.createArray()
|
||||||
for(item in it) {
|
for (item in it) {
|
||||||
threadsArray.pushMap(item)
|
threadsArray.pushMap(item)
|
||||||
}
|
}
|
||||||
notificationData.putArray("threads", threadsArray)
|
notificationData.putArray("threads", threadsArray)
|
||||||
|
|
@ -105,7 +104,7 @@ class PushNotificationDataRunnable {
|
||||||
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
dbHelper.saveToDatabase(db, notificationData, teamId, channelId, receivingThreads)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId")
|
Log.i("ReactNative", "Done processing push notification=$serverUrl for channel=$channelId and ack=$ackId")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.mattermost.helpers.database_extension
|
package com.mattermost.helpers.database_extension
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.facebook.react.bridge.Arguments
|
import com.facebook.react.bridge.Arguments
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
|
@ -8,8 +9,7 @@ import com.mattermost.helpers.DatabaseHelper
|
||||||
import com.mattermost.helpers.QueryArgs
|
import com.mattermost.helpers.QueryArgs
|
||||||
import com.mattermost.helpers.mapCursor
|
import com.mattermost.helpers.mapCursor
|
||||||
import com.nozbe.watermelondb.WMDatabase
|
import com.nozbe.watermelondb.WMDatabase
|
||||||
import java.util.*
|
import java.util.Arrays
|
||||||
import kotlin.Exception
|
|
||||||
|
|
||||||
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||||
db.transaction {
|
db.transaction {
|
||||||
|
|
@ -57,7 +57,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W
|
||||||
if (cursor.count == 1) {
|
if (cursor.count == 1) {
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
val databasePath = String.format("file://%s", cursor.getString(0))
|
val databasePath = String.format("file://%s", cursor.getString(0))
|
||||||
return WMDatabase.getInstance(databasePath, context!!)
|
return WMDatabase.buildDatabase(databasePath, context!!, SQLiteDatabase.CREATE_IF_NECESSARY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -73,7 +73,7 @@ fun DatabaseHelper.getDeviceToken(): String? {
|
||||||
defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor ->
|
defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor ->
|
||||||
if (cursor.count == 1) {
|
if (cursor.count == 1) {
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
return cursor.getString(0);
|
return cursor.getString(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
package com.mattermost.rnbeta;
|
|
||||||
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import com.facebook.react.bridge.ReadableMap;
|
|
||||||
import com.mattermost.helpers.CustomPushNotificationHelper;
|
|
||||||
import com.mattermost.helpers.DatabaseHelper;
|
|
||||||
import com.mattermost.helpers.Network;
|
|
||||||
import com.mattermost.helpers.NotificationHelper;
|
|
||||||
import com.mattermost.helpers.PushNotificationDataHelper;
|
|
||||||
import com.mattermost.helpers.ReadableMapUtils;
|
|
||||||
import com.mattermost.share.ShareModule;
|
|
||||||
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
|
|
||||||
import com.wix.reactnativenotifications.core.notification.PushNotification;
|
|
||||||
import com.wix.reactnativenotifications.core.AppLaunchHelper;
|
|
||||||
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
|
|
||||||
import com.wix.reactnativenotifications.core.JsIOHelper;
|
|
||||||
|
|
||||||
import static com.mattermost.helpers.database_extension.GeneralKt.*;
|
|
||||||
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
|
|
||||||
|
|
||||||
|
|
||||||
public class CustomPushNotification extends PushNotification {
|
|
||||||
private final PushNotificationDataHelper dataHelper;
|
|
||||||
|
|
||||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
|
||||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
|
||||||
dataHelper = new PushNotificationDataHelper(context);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Objects.requireNonNull(DatabaseHelper.Companion.getInstance()).init(context);
|
|
||||||
Network.init(context);
|
|
||||||
NotificationHelper.cleanNotificationPreferencesIfNeeded(context);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceived() {
|
|
||||||
final Bundle initialData = mNotificationProps.asBundle();
|
|
||||||
final String type = initialData.getString("type");
|
|
||||||
final String ackId = initialData.getString("ack_id");
|
|
||||||
final String postId = initialData.getString("post_id");
|
|
||||||
final String channelId = initialData.getString("channel_id");
|
|
||||||
final String signature = initialData.getString("signature");
|
|
||||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
|
|
||||||
int notificationId = NotificationHelper.getNotificationId(initialData);
|
|
||||||
|
|
||||||
String serverUrl = addServerUrlToBundle(initialData);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
current.putAll(response);
|
|
||||||
mNotificationProps = createProps(current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) {
|
|
||||||
Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
|
||||||
ShareModule shareModule = ShareModule.getInstance();
|
|
||||||
String currentActivityName = shareModule != null ? shareModule.getCurrentActivityName() : "";
|
|
||||||
Log.i("ReactNative", currentActivityName);
|
|
||||||
if (!mAppLifecycleFacade.isAppVisible() || !currentActivityName.equals("MainActivity")) {
|
|
||||||
boolean createSummary = type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE);
|
|
||||||
if (type.equals(CustomPushNotificationHelper.PUSH_TYPE_MESSAGE)) {
|
|
||||||
if (channelId != null) {
|
|
||||||
Bundle notificationBundle = mNotificationProps.asBundle();
|
|
||||||
if (serverUrl != null) {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
createSummary = NotificationHelper.addNotificationToPreferences(
|
|
||||||
mContext,
|
|
||||||
notificationId,
|
|
||||||
notificationBundle
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildNotification(notificationId, createSummary);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CustomPushNotificationHelper.PUSH_TYPE_CLEAR:
|
|
||||||
NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isReactInit) {
|
|
||||||
notifyReceivedToJS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildNotification(Integer notificationId, boolean createSummary) {
|
|
||||||
final PendingIntent pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps);
|
|
||||||
final Notification notification = buildNotification(pendingIntent);
|
|
||||||
if (createSummary) {
|
|
||||||
final Notification summary = getNotificationSummaryBuilder(pendingIntent).build();
|
|
||||||
super.postNotification(summary, notificationId + 1);
|
|
||||||
}
|
|
||||||
super.postNotification(notification, notificationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected NotificationCompat.Builder getNotificationBuilder(PendingIntent intent) {
|
|
||||||
Bundle bundle = mNotificationProps.asBundle();
|
|
||||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected NotificationCompat.Builder getNotificationSummaryBuilder(PendingIntent intent) {
|
|
||||||
Bundle bundle = mNotificationProps.asBundle();
|
|
||||||
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(serverUrl)) {
|
|
||||||
bundle.putString("server_url", serverUrl);
|
|
||||||
mNotificationProps = createProps(bundle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
package com.mattermost.rnbeta
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
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.database_extension.getServerUrlForIdentifier
|
||||||
|
import com.mattermost.share.ShareModule
|
||||||
|
import com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME
|
||||||
|
import com.wix.reactnativenotifications.core.AppLaunchHelper
|
||||||
|
import com.wix.reactnativenotifications.core.AppLifecycleFacade
|
||||||
|
import com.wix.reactnativenotifications.core.JsIOHelper
|
||||||
|
import com.wix.reactnativenotifications.core.NotificationIntentAdapter
|
||||||
|
import com.wix.reactnativenotifications.core.notification.PushNotification
|
||||||
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class CustomPushNotification(
|
||||||
|
context: Context,
|
||||||
|
bundle: Bundle,
|
||||||
|
appLifecycleFacade: AppLifecycleFacade,
|
||||||
|
appLaunchHelper: AppLaunchHelper,
|
||||||
|
jsIoHelper: JsIOHelper
|
||||||
|
) : PushNotification(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper) {
|
||||||
|
private val dataHelper = PushNotificationDataHelper(context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
DatabaseHelper.instance?.init(context)
|
||||||
|
Network.init(context)
|
||||||
|
NotificationHelper.cleanNotificationPreferencesIfNeeded(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
override fun onReceived() {
|
||||||
|
val initialData = mNotificationProps.asBundle()
|
||||||
|
val type = initialData.getString("type")
|
||||||
|
val ackId = initialData.getString("ack_id")
|
||||||
|
val postId = initialData.getString("post_id")
|
||||||
|
val channelId = initialData.getString("channel_id")
|
||||||
|
val signature = initialData.getString("signature")
|
||||||
|
val isIdLoaded = initialData.getString("id_loaded") == "true"
|
||||||
|
val notificationId = NotificationHelper.getNotificationId(initialData)
|
||||||
|
val serverUrl = addServerUrlToBundle(initialData)
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
try {
|
||||||
|
handlePushNotificationInCoroutine(serverUrl, type, channelId, ackId, isIdLoaded, notificationId, postId, signature)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handlePushNotificationInCoroutine(
|
||||||
|
serverUrl: String?,
|
||||||
|
type: String?,
|
||||||
|
channelId: String?,
|
||||||
|
ackId: String?,
|
||||||
|
isIdLoaded: Boolean,
|
||||||
|
notificationId: Int,
|
||||||
|
postId: String?,
|
||||||
|
signature: String?
|
||||||
|
) {
|
||||||
|
if (ackId != null && serverUrl != null) {
|
||||||
|
val response = ReceiptDelivery.send(ackId, serverUrl, postId, type, isIdLoaded)
|
||||||
|
if (isIdLoaded && response != null) {
|
||||||
|
val current = mNotificationProps.asBundle()
|
||||||
|
if (!current.containsKey("server_url")) {
|
||||||
|
response.putString("server_url", serverUrl)
|
||||||
|
}
|
||||||
|
current.putAll(response)
|
||||||
|
mNotificationProps = createProps(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CustomPushNotificationHelper.verifySignature(mContext, signature, serverUrl, ackId)) {
|
||||||
|
Log.i("Mattermost Notifications Signature verification", "Notification skipped because we could not verify it.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
finishProcessingNotification(serverUrl, type, channelId, notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpened() {
|
||||||
|
mNotificationProps?.let {
|
||||||
|
digestNotification()
|
||||||
|
NotificationHelper.clearChannelOrThreadNotifications(mContext, it.asBundle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun finishProcessingNotification(serverUrl: String?, type: String?, channelId: String?, notificationId: Int) {
|
||||||
|
val isReactInit = mAppLifecycleFacade.isReactInitialized()
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
CustomPushNotificationHelper.PUSH_TYPE_MESSAGE, CustomPushNotificationHelper.PUSH_TYPE_SESSION -> {
|
||||||
|
val shareModule = ShareModule.getInstance()
|
||||||
|
val currentActivityName = shareModule?.currentActivityName ?: ""
|
||||||
|
Log.i("ReactNative", currentActivityName)
|
||||||
|
if (!mAppLifecycleFacade.isAppVisible() || currentActivityName != "MainActivity") {
|
||||||
|
var createSummary = type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE
|
||||||
|
if (type == CustomPushNotificationHelper.PUSH_TYPE_MESSAGE) {
|
||||||
|
channelId?.let {
|
||||||
|
val notificationBundle = mNotificationProps.asBundle()
|
||||||
|
serverUrl?.let {
|
||||||
|
val notificationResult = dataHelper.fetchAndStoreDataForPushNotification(notificationBundle, isReactInit)
|
||||||
|
notificationResult?.let { result ->
|
||||||
|
notificationBundle.putBundle("data", result)
|
||||||
|
mNotificationProps = createProps(notificationBundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createSummary = NotificationHelper.addNotificationToPreferences(mContext, notificationId, notificationBundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildNotification(notificationId, createSummary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CustomPushNotificationHelper.PUSH_TYPE_CLEAR -> NotificationHelper.clearChannelOrThreadNotifications(mContext, mNotificationProps.asBundle())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReactInit) {
|
||||||
|
notifyReceivedToJS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(notificationId: Int, createSummary: Boolean) {
|
||||||
|
val pendingIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, mNotificationProps)
|
||||||
|
val notification = buildNotification(pendingIntent)
|
||||||
|
if (createSummary) {
|
||||||
|
val summary = getNotificationSummaryBuilder(pendingIntent).build()
|
||||||
|
super.postNotification(summary, notificationId + 1)
|
||||||
|
}
|
||||||
|
super.postNotification(notification, notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNotificationBuilder(intent: PendingIntent): NotificationCompat.Builder {
|
||||||
|
val bundle = mNotificationProps.asBundle()
|
||||||
|
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNotificationSummaryBuilder(intent: PendingIntent): NotificationCompat.Builder {
|
||||||
|
val bundle = mNotificationProps.asBundle()
|
||||||
|
return CustomPushNotificationHelper.createNotificationBuilder(mContext, intent, bundle, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyReceivedToJS() {
|
||||||
|
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.runningReactContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addServerUrlToBundle(bundle: Bundle): String? {
|
||||||
|
val dbHelper = DatabaseHelper.instance
|
||||||
|
val serverId = bundle.getString("server_id")
|
||||||
|
var serverUrl: String? = null
|
||||||
|
|
||||||
|
dbHelper?.let {
|
||||||
|
serverUrl = if (serverId == null) {
|
||||||
|
it.onlyServerUrl
|
||||||
|
} else {
|
||||||
|
it.getServerUrlForIdentifier(serverId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverUrl.isNullOrEmpty()) {
|
||||||
|
bundle.putString("server_url", serverUrl)
|
||||||
|
mNotificationProps = createProps(bundle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -173,10 +173,10 @@ class MainApplication : NavigationApplication(), INotificationsApplication {
|
||||||
defaultAppLaunchHelper: AppLaunchHelper?
|
defaultAppLaunchHelper: AppLaunchHelper?
|
||||||
): IPushNotification {
|
): IPushNotification {
|
||||||
return CustomPushNotification(
|
return CustomPushNotification(
|
||||||
context,
|
context!!,
|
||||||
bundle,
|
bundle!!,
|
||||||
defaultFacade,
|
defaultFacade!!,
|
||||||
defaultAppLaunchHelper,
|
defaultAppLaunchHelper!!,
|
||||||
JsIOHelper()
|
JsIOHelper()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
handleCallUserVoiceOn,
|
handleCallUserVoiceOn,
|
||||||
handleHostLowerHand,
|
handleHostLowerHand,
|
||||||
handleHostMute,
|
handleHostMute,
|
||||||
|
handleHostRemoved,
|
||||||
handleUserDismissedNotification,
|
handleUserDismissedNotification,
|
||||||
} from '@calls/connection/websocket_event_handlers';
|
} from '@calls/connection/websocket_event_handlers';
|
||||||
import {isSupportedServerCalls} from '@calls/utils';
|
import {isSupportedServerCalls} from '@calls/utils';
|
||||||
|
|
@ -461,6 +462,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||||
case WebsocketEvents.CALLS_HOST_LOWER_HAND:
|
case WebsocketEvents.CALLS_HOST_LOWER_HAND:
|
||||||
handleHostLowerHand(serverUrl, msg);
|
handleHostLowerHand(serverUrl, msg);
|
||||||
break;
|
break;
|
||||||
|
case WebsocketEvents.CALLS_HOST_REMOVED:
|
||||||
|
handleHostRemoved(serverUrl, msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case WebsocketEvents.GROUP_RECEIVED:
|
case WebsocketEvents.GROUP_RECEIVED:
|
||||||
handleGroupReceivedEvent(serverUrl, msg);
|
handleGroupReceivedEvent(serverUrl, msg);
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ const AtMention = ({
|
||||||
const filteredUsers = filterResults(fallbackUsers, term);
|
const filteredUsers = filterResults(fallbackUsers, term);
|
||||||
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
|
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
|
||||||
} else if (receivedUsers) {
|
} else if (receivedUsers) {
|
||||||
sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
|
await sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -298,8 +298,14 @@ const AtMention = ({
|
||||||
|
|
||||||
if (outOfChannel?.length) {
|
if (outOfChannel?.length) {
|
||||||
const outChannelMemberIds = outOfChannel.map((e) => e.id);
|
const outChannelMemberIds = outOfChannel.map((e) => e.id);
|
||||||
|
|
||||||
|
// This only get us the users we have on the database.
|
||||||
|
// We need to append those users from which we don't have
|
||||||
|
// information at the end of the list.
|
||||||
const outSortedMembers = await getUsersFromDMSorted(database, outChannelMemberIds);
|
const outSortedMembers = await getUsersFromDMSorted(database, outChannelMemberIds);
|
||||||
sortedMembers.push(...outSortedMembers);
|
const idSet = new Set(outSortedMembers.map((v) => v.id));
|
||||||
|
const outRest = outOfChannel.filter((v) => !idSet.has(v.id));
|
||||||
|
sortedMembers.push(...outSortedMembers, ...outRest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasTrailingSpaces(term)) {
|
if (hasTrailingSpaces(term)) {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {getLastPictureUpdate} from '@utils/user';
|
||||||
|
|
||||||
import type UserModel from '@typings/database/models/servers/user';
|
import type UserModel from '@typings/database/models/servers/user';
|
||||||
|
|
||||||
|
|
@ -42,6 +43,7 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
|
||||||
serverUrl = url || serverUrl;
|
serverUrl = url || serverUrl;
|
||||||
|
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
|
const lastPictureUpdateAt = author ? getLastPictureUpdate(author) : 0;
|
||||||
const fIStyle = useMemo(() => ({
|
const fIStyle = useMemo(() => ({
|
||||||
borderRadius: size / 2,
|
borderRadius: size / 2,
|
||||||
backgroundColor: theme.centerChannelBg,
|
backgroundColor: theme.centerChannelBg,
|
||||||
|
|
@ -56,7 +58,7 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
|
||||||
|
|
||||||
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
|
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
|
||||||
return source ?? {uri: buildAbsoluteUrl(serverUrl, pictureUrl)};
|
return source ?? {uri: buildAbsoluteUrl(serverUrl, pictureUrl)};
|
||||||
}, [author, serverUrl, source]);
|
}, [author, serverUrl, source, lastPictureUpdateAt]);
|
||||||
|
|
||||||
if (typeof source === 'string') {
|
if (typeof source === 'string') {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ const WebsocketEvents = {
|
||||||
CALLS_CAPTION: `custom_${Calls.PluginId}_caption`,
|
CALLS_CAPTION: `custom_${Calls.PluginId}_caption`,
|
||||||
CALLS_HOST_MUTE: `custom_${Calls.PluginId}_host_mute`,
|
CALLS_HOST_MUTE: `custom_${Calls.PluginId}_host_mute`,
|
||||||
CALLS_HOST_LOWER_HAND: `custom_${Calls.PluginId}_host_lower_hand`,
|
CALLS_HOST_LOWER_HAND: `custom_${Calls.PluginId}_host_lower_hand`,
|
||||||
|
CALLS_HOST_REMOVED: `custom_${Calls.PluginId}_host_removed`,
|
||||||
|
|
||||||
GROUP_RECEIVED: 'received_group',
|
GROUP_RECEIVED: 'received_group',
|
||||||
GROUP_MEMBER_ADD: 'group_member_add',
|
GROUP_MEMBER_ADD: 'group_member_add',
|
||||||
|
|
|
||||||
|
|
@ -642,6 +642,17 @@ export const hostMuteSession = async (serverUrl: string, callId: string, session
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hostMuteOthers = async (serverUrl: string, callId: string) => {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
return client.hostMuteOthers(callId);
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on hostMuteOthers', getFullErrorMessage(error));
|
||||||
|
await forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const hostStopScreenshare = async (serverUrl: string, callId: string, sessionId: string) => {
|
export const hostStopScreenshare = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||||
try {
|
try {
|
||||||
const client = NetworkManager.getClient(serverUrl);
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
|
@ -664,3 +675,13 @@ export const hostLowerHand = async (serverUrl: string, callId: string, sessionId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hostRemove = async (serverUrl: string, callId: string, sessionId: string) => {
|
||||||
|
try {
|
||||||
|
const client = NetworkManager.getClient(serverUrl);
|
||||||
|
return client.hostRemove(callId, sessionId);
|
||||||
|
} catch (error) {
|
||||||
|
logDebug('error on hostRemove', getFullErrorMessage(error));
|
||||||
|
await forceLogoutIfNecessary(serverUrl, error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
import {Alert} from 'react-native';
|
import {Alert} from 'react-native';
|
||||||
|
|
||||||
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
|
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
|
||||||
import {dismissIncomingCall} from '@calls/actions/calls';
|
import {dismissIncomingCall, hostRemove} from '@calls/actions/calls';
|
||||||
import {hasBluetoothPermission} from '@calls/actions/permissions';
|
import {hasBluetoothPermission} from '@calls/actions/permissions';
|
||||||
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
import {hostRemovedErr, userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||||
import {
|
import {
|
||||||
getCallsConfig,
|
getCallsConfig,
|
||||||
getCallsState,
|
getCallsState,
|
||||||
|
|
@ -19,6 +19,7 @@ import {errorAlert} from '@calls/utils';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {getChannelById} from '@queries/servers/channel';
|
import {getChannelById} from '@queries/servers/channel';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import {isDMorGM} from '@utils/channel';
|
import {isDMorGM} from '@utils/channel';
|
||||||
import {getFullErrorMessage} from '@utils/errors';
|
import {getFullErrorMessage} from '@utils/errors';
|
||||||
import {logError} from '@utils/log';
|
import {logError} from '@utils/log';
|
||||||
|
|
@ -413,8 +414,55 @@ export const showErrorAlertOnClose = (err: Error, intl: IntlShape) => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case hostRemovedErr:
|
||||||
|
Alert.alert(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'mobile.calls_removed_alert_title',
|
||||||
|
defaultMessage: 'You were removed from the call',
|
||||||
|
}),
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'mobile.calls_removed_alert_body',
|
||||||
|
defaultMessage: 'The host removed you from the call.',
|
||||||
|
}),
|
||||||
|
[{
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'mobile.calls_dismiss',
|
||||||
|
defaultMessage: 'Dismiss',
|
||||||
|
}),
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Fallback with generic error
|
// Fallback with generic error
|
||||||
errorAlert(getFullErrorMessage(err, intl), intl);
|
errorAlert(getFullErrorMessage(err, intl), intl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeFromCall = (serverUrl: string, displayName: string, callId: string, sessionId: string, intl: IntlShape) => {
|
||||||
|
const {formatMessage} = intl;
|
||||||
|
|
||||||
|
const title = formatMessage({
|
||||||
|
id: 'mobile.calls_remove_alert_title',
|
||||||
|
defaultMessage: 'Remove participant',
|
||||||
|
});
|
||||||
|
const body = formatMessage({
|
||||||
|
id: 'mobile.calls_remove_alert_body',
|
||||||
|
defaultMessage: 'Are you sure you want to remove {displayName} from the call? ',
|
||||||
|
}, {displayName});
|
||||||
|
|
||||||
|
Alert.alert(title, body, [{
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'mobile.post.cancel',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
}),
|
||||||
|
}, {
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'mobile.calls_remove',
|
||||||
|
defaultMessage: 'Remove',
|
||||||
|
}),
|
||||||
|
onPress: () => {
|
||||||
|
hostRemove(serverUrl, callId, sessionId);
|
||||||
|
dismissBottomSheet();
|
||||||
|
},
|
||||||
|
style: 'destructive',
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ export interface ClientCallsMix {
|
||||||
dismissCall: (channelId: string) => Promise<ApiResp>;
|
dismissCall: (channelId: string) => Promise<ApiResp>;
|
||||||
hostMake: (callId: string, newHostId: string) => Promise<ApiResp>;
|
hostMake: (callId: string, newHostId: string) => Promise<ApiResp>;
|
||||||
hostMute: (callId: string, sessionId: string) => Promise<ApiResp>;
|
hostMute: (callId: string, sessionId: string) => Promise<ApiResp>;
|
||||||
|
hostMuteOthers: (callId: string) => Promise<ApiResp>;
|
||||||
hostScreenOff: (callId: string, sessionId: string) => Promise<ApiResp>;
|
hostScreenOff: (callId: string, sessionId: string) => Promise<ApiResp>;
|
||||||
hostLowerHand: (callId: string, sessionId: string) => Promise<ApiResp>;
|
hostLowerHand: (callId: string, sessionId: string) => Promise<ApiResp>;
|
||||||
|
hostRemove: (callId: string, sessionId: string) => Promise<ApiResp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientCalls = (superclass: any) => class extends superclass {
|
const ClientCalls = (superclass: any) => class extends superclass {
|
||||||
|
|
@ -130,6 +132,13 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hostMuteOthers = async (callId: string) => {
|
||||||
|
return this.doFetch(
|
||||||
|
`${this.getCallsRoute()}/calls/${callId}/host/mute-others`,
|
||||||
|
{method: 'post'},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
hostScreenOff = async (callId: string, sessionId: string) => {
|
hostScreenOff = async (callId: string, sessionId: string) => {
|
||||||
return this.doFetch(
|
return this.doFetch(
|
||||||
`${this.getCallsRoute()}/calls/${callId}/host/screen-off`,
|
`${this.getCallsRoute()}/calls/${callId}/host/screen-off`,
|
||||||
|
|
@ -149,6 +158,16 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hostRemove = async (callId: string, sessionId: string) => {
|
||||||
|
return this.doFetch(
|
||||||
|
`${this.getCallsRoute()}/calls/${callId}/host/remove`,
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
body: {session_id: sessionId},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ClientCalls;
|
export default ClientCalls;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
import {DeviceEventEmitter} from 'react-native';
|
import {DeviceEventEmitter} from 'react-native';
|
||||||
|
|
||||||
import {fetchUsersByIds} from '@actions/remote/user';
|
import {fetchUsersByIds} from '@actions/remote/user';
|
||||||
import {muteMyself, unraiseHand} from '@calls/actions';
|
import {leaveCall, muteMyself, unraiseHand} from '@calls/actions';
|
||||||
|
import {hostRemovedErr} from '@calls/errors';
|
||||||
import {
|
import {
|
||||||
callEnded,
|
callEnded,
|
||||||
callStarted,
|
callStarted,
|
||||||
|
|
@ -228,3 +229,14 @@ export const handleHostLowerHand = async (serverUrl: string, msg: WebSocketMessa
|
||||||
|
|
||||||
unraiseHand();
|
unraiseHand();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleHostRemoved = async (serverUrl: string, msg: WebSocketMessage<HostControlsMsgData>) => {
|
||||||
|
const currentCall = getCurrentCall();
|
||||||
|
if (currentCall?.serverUrl !== serverUrl ||
|
||||||
|
currentCall?.channelId !== msg.data.channel_id ||
|
||||||
|
currentCall?.mySessionId !== msg.data.session_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveCall(hostRemovedErr);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@
|
||||||
|
|
||||||
export const userRemovedFromChannelErr = new Error('user was removed from channel');
|
export const userRemovedFromChannelErr = new Error('user was removed from channel');
|
||||||
export const userLeftChannelErr = new Error('user has left channel');
|
export const userLeftChannelErr = new Error('user has left channel');
|
||||||
|
export const hostRemovedErr = new Error('user was removed by host');
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,28 @@
|
||||||
|
|
||||||
import React, {useCallback, useMemo} from 'react';
|
import React, {useCallback, useMemo} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {StyleSheet} from 'react-native';
|
import {Platform, View} from 'react-native';
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import {hostLowerHand, hostMake, hostMuteSession, hostStopScreenshare} from '@calls/actions/calls';
|
import {hostLowerHand, hostMake, hostMuteSession, hostStopScreenshare} from '@calls/actions/calls';
|
||||||
|
import {removeFromCall} from '@calls/alerts';
|
||||||
|
import {useHostMenus} from '@calls/hooks';
|
||||||
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
import SlideUpPanelItem from '@components/slide_up_panel_item';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
|
import {useTheme} from '@context/theme';
|
||||||
import BottomSheet from '@screens/bottom_sheet';
|
import BottomSheet from '@screens/bottom_sheet';
|
||||||
import {dismissBottomSheet} from '@screens/navigation';
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import UserProfileTitle from '@screens/user_profile/title';
|
import UserProfileTitle from '@screens/user_profile/title';
|
||||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||||
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
import {displayUsername} from '@utils/user';
|
||||||
|
|
||||||
import type {CallSession, CurrentCall} from '@calls/types/calls';
|
import type {CallSession, CurrentCall} from '@calls/types/calls';
|
||||||
|
|
||||||
const TITLE_HEIGHT = 118;
|
const TITLE_HEIGHT = 118;
|
||||||
const ITEM_HEIGHT = 48;
|
const ITEM_HEIGHT = 48;
|
||||||
|
const SEPARATOR_HEIGHT = 17;
|
||||||
|
const ANDROID_BUMP_HEIGHT = 20;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentCall: CurrentCall;
|
currentCall: CurrentCall;
|
||||||
|
|
@ -27,11 +34,18 @@ type Props = {
|
||||||
hideGuestTags: boolean;
|
hideGuestTags: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
iconStyle: {
|
iconStyle: {
|
||||||
marginRight: 6,
|
marginRight: 6,
|
||||||
},
|
},
|
||||||
});
|
separator: {
|
||||||
|
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||||
|
height: 1,
|
||||||
|
width: '100%',
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginVertical: 8,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const HostControls = ({
|
export const HostControls = ({
|
||||||
currentCall,
|
currentCall,
|
||||||
|
|
@ -41,7 +55,10 @@ export const HostControls = ({
|
||||||
hideGuestTags,
|
hideGuestTags,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const theme = useTheme();
|
||||||
const {bottom} = useSafeAreaInsets();
|
const {bottom} = useSafeAreaInsets();
|
||||||
|
const {openUserProfile} = useHostMenus();
|
||||||
|
const styles = getStyleFromTheme(theme);
|
||||||
|
|
||||||
const sharingScreen = currentCall.screenOn === session.sessionId;
|
const sharingScreen = currentCall.screenOn === session.sessionId;
|
||||||
|
|
||||||
|
|
@ -63,20 +80,35 @@ export const HostControls = ({
|
||||||
const stopScreensharePress = useCallback(async () => {
|
const stopScreensharePress = useCallback(async () => {
|
||||||
hostStopScreenshare(currentCall.serverUrl, currentCall.channelId, session.sessionId);
|
hostStopScreenshare(currentCall.serverUrl, currentCall.channelId, session.sessionId);
|
||||||
await dismissBottomSheet();
|
await dismissBottomSheet();
|
||||||
}, [currentCall.serverUrl, currentCall.id, session.sessionId]);
|
}, [currentCall.serverUrl, currentCall.channelId, session.sessionId]);
|
||||||
|
|
||||||
|
const profilePress = useCallback(async () => {
|
||||||
|
await dismissBottomSheet();
|
||||||
|
openUserProfile(session);
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const removePress = useCallback(async () => {
|
||||||
|
const displayName = displayUsername(session.userModel, intl.locale, teammateNameDisplay, true);
|
||||||
|
removeFromCall(currentCall.serverUrl, displayName, currentCall.channelId, session.sessionId, intl);
|
||||||
|
}, [session.userModel, intl, teammateNameDisplay, currentCall.serverUrl, currentCall.channelId, session.sessionId]);
|
||||||
|
|
||||||
const snapPoints = useMemo(() => {
|
const snapPoints = useMemo(() => {
|
||||||
const items = 1 + (session.muted ? 0 : 1) + (sharingScreen ? 1 : 0) + (session.raisedHand ? 1 : 0);
|
const items = 3 + (session.muted ? 0 : 1) + (sharingScreen ? 1 : 0) + (session.raisedHand ? 1 : 0);
|
||||||
return [
|
return [
|
||||||
1,
|
1,
|
||||||
bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + TITLE_HEIGHT,
|
bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom) + TITLE_HEIGHT + SEPARATOR_HEIGHT + (Platform.OS === 'android' ? ANDROID_BUMP_HEIGHT : 0),
|
||||||
];
|
];
|
||||||
}, [bottom, session.muted, sharingScreen, session.raisedHand]);
|
}, [bottom, session.muted, sharingScreen, session.raisedHand]);
|
||||||
|
|
||||||
const makeHostText = intl.formatMessage({id: 'mobile.calls_make_host', defaultMessage: 'Make host'});
|
const makeHostText = intl.formatMessage({id: 'mobile.calls_make_host', defaultMessage: 'Make host'});
|
||||||
const muteText = intl.formatMessage({id: 'mobile.calls_mute_participant', defaultMessage: 'Mute participant'});
|
const muteText = intl.formatMessage({id: 'mobile.calls_mute_participant', defaultMessage: 'Mute participant'});
|
||||||
const lowerHandText = intl.formatMessage({id: 'mobile.calls_lower_hand', defaultMessage: 'Lower hand'});
|
const lowerHandText = intl.formatMessage({id: 'mobile.calls_lower_hand', defaultMessage: 'Lower hand'});
|
||||||
const stopScreenshareText = intl.formatMessage({id: 'mobile.calls_stop_screenshare', defaultMessage: 'Stop screen share'});
|
const stopScreenshareText = intl.formatMessage({
|
||||||
|
id: 'mobile.calls_stop_screenshare',
|
||||||
|
defaultMessage: 'Stop screen share',
|
||||||
|
});
|
||||||
|
const profileText = intl.formatMessage({id: 'mobile.calls_view_profile', defaultMessage: 'View profile'});
|
||||||
|
const removeText = intl.formatMessage({id: 'mobile.calls_remove_participant', defaultMessage: 'Remove from call'});
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!session?.userModel) {
|
if (!session?.userModel) {
|
||||||
|
|
@ -125,6 +157,20 @@ export const HostControls = ({
|
||||||
onPress={makeHostPress}
|
onPress={makeHostPress}
|
||||||
text={makeHostText}
|
text={makeHostText}
|
||||||
/>
|
/>
|
||||||
|
<SlideUpPanelItem
|
||||||
|
leftIcon={'account-outline'}
|
||||||
|
leftIconStyles={styles.iconStyle}
|
||||||
|
onPress={profilePress}
|
||||||
|
text={profileText}
|
||||||
|
/>
|
||||||
|
<View style={styles.separator}/>
|
||||||
|
<SlideUpPanelItem
|
||||||
|
leftIcon={'minus-circle-outline'}
|
||||||
|
leftIconStyles={styles.iconStyle}
|
||||||
|
onPress={removePress}
|
||||||
|
text={removeText}
|
||||||
|
destructive={true}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators';
|
||||||
|
|
||||||
import {observeCallDatabase, observeCurrentSessionsDict} from '@calls/observers';
|
import {observeCallDatabase, observeCurrentSessionsDict} from '@calls/observers';
|
||||||
import {ParticipantsList} from '@calls/screens/participants_list/participants_list';
|
import {ParticipantsList} from '@calls/screens/participants_list/participants_list';
|
||||||
|
import {observeCurrentCall} from '@calls/state';
|
||||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||||
|
|
||||||
const enhanced = withObservables([], () => {
|
const enhanced = withObservables([], () => {
|
||||||
|
|
@ -14,10 +15,22 @@ const enhanced = withObservables([], () => {
|
||||||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
);
|
);
|
||||||
|
const callServerUrl = observeCurrentCall().pipe(
|
||||||
|
switchMap((call) => of$(call?.serverUrl)),
|
||||||
|
);
|
||||||
|
const callChannelId = observeCurrentCall().pipe(
|
||||||
|
switchMap((call) => of$(call?.channelId)),
|
||||||
|
);
|
||||||
|
const callUserId = observeCurrentCall().pipe(
|
||||||
|
switchMap((call) => of$(call?.myUserId)),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionsDict: observeCurrentSessionsDict(),
|
sessionsDict: observeCurrentSessionsDict(),
|
||||||
teammateNameDisplay,
|
teammateNameDisplay,
|
||||||
|
callServerUrl,
|
||||||
|
callChannelId,
|
||||||
|
callUserId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@
|
||||||
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
|
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
|
||||||
import React, {useCallback, useMemo} from 'react';
|
import React, {useCallback, useMemo} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {type ListRenderItemInfo, useWindowDimensions, View} from 'react-native';
|
import {type ListRenderItemInfo, Text, TouchableOpacity, useWindowDimensions, View} from 'react-native';
|
||||||
import {FlatList} from 'react-native-gesture-handler';
|
import {FlatList} from 'react-native-gesture-handler';
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import {useHostMenus} from '@calls/hooks';
|
import {hostMuteOthers} from '@calls/actions/calls';
|
||||||
|
import {useHostControlsAvailable, useHostMenus} from '@calls/hooks';
|
||||||
import {Participant} from '@calls/screens/participants_list/participant';
|
import {Participant} from '@calls/screens/participants_list/participant';
|
||||||
import Pill from '@calls/screens/participants_list/pill';
|
import Pill from '@calls/screens/participants_list/pill';
|
||||||
import {sortSessions} from '@calls/utils';
|
import {sortSessions} from '@calls/utils';
|
||||||
|
import CompassIcon from '@components/compass_icon';
|
||||||
import FormattedText from '@components/formatted_text';
|
import FormattedText from '@components/formatted_text';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
|
@ -31,11 +33,15 @@ type Props = {
|
||||||
closeButtonId: string;
|
closeButtonId: string;
|
||||||
sessionsDict: Dictionary<CallSession>;
|
sessionsDict: Dictionary<CallSession>;
|
||||||
teammateNameDisplay: string;
|
teammateNameDisplay: string;
|
||||||
|
callServerUrl?: string;
|
||||||
|
callChannelId?: string;
|
||||||
|
callUserId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
header: {
|
header: {
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
|
paddingRight: 2,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -44,12 +50,35 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
color: theme.centerChannelColor,
|
color: theme.centerChannelColor,
|
||||||
...typography('Heading', 600, 'SemiBold'),
|
...typography('Heading', 600, 'SemiBold'),
|
||||||
},
|
},
|
||||||
|
muteButton: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 6,
|
||||||
|
gap: 6,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
muteIcon: {
|
||||||
|
color: theme.buttonBg,
|
||||||
|
},
|
||||||
|
muteText: {
|
||||||
|
color: theme.buttonBg,
|
||||||
|
...typography('Body', 100, 'SemiBold'),
|
||||||
|
marginTop: -2,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDisplay}: Props) => {
|
export const ParticipantsList = ({
|
||||||
|
closeButtonId,
|
||||||
|
sessionsDict,
|
||||||
|
teammateNameDisplay,
|
||||||
|
callServerUrl,
|
||||||
|
callChannelId,
|
||||||
|
callUserId,
|
||||||
|
}: Props) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {onPress} = useHostMenus();
|
const {onPress} = useHostMenus();
|
||||||
|
const hostControlsAvailable = useHostControlsAvailable();
|
||||||
const {bottom} = useSafeAreaInsets();
|
const {bottom} = useSafeAreaInsets();
|
||||||
const {height} = useWindowDimensions();
|
const {height} = useWindowDimensions();
|
||||||
const isTablet = useIsTablet();
|
const isTablet = useIsTablet();
|
||||||
|
|
@ -63,6 +92,14 @@ export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDispl
|
||||||
if (sessions.length > MIN_ROWS && snapPoint1 < snapPoint2) {
|
if (sessions.length > MIN_ROWS && snapPoint1 < snapPoint2) {
|
||||||
snapPoints.push(snapPoint2);
|
snapPoints.push(snapPoint2);
|
||||||
}
|
}
|
||||||
|
const otherUnMuted = useMemo(() => sessions.some((s) => !s.muted && s.userId !== callUserId), [sessions, callUserId]);
|
||||||
|
|
||||||
|
const muteOthersPress = useCallback(async () => {
|
||||||
|
if (!callServerUrl || !callChannelId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hostMuteOthers(callServerUrl, callChannelId);
|
||||||
|
}, [callServerUrl, callChannelId]);
|
||||||
|
|
||||||
const renderItem = useCallback(({item}: ListRenderItemInfo<CallSession>) => (
|
const renderItem = useCallback(({item}: ListRenderItemInfo<CallSession>) => (
|
||||||
<Participant
|
<Participant
|
||||||
|
|
@ -83,6 +120,21 @@ export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDispl
|
||||||
defaultMessage={'Participants'}
|
defaultMessage={'Participants'}
|
||||||
/>
|
/>
|
||||||
<Pill text={sessions.length}/>
|
<Pill text={sessions.length}/>
|
||||||
|
{hostControlsAvailable && otherUnMuted &&
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.muteButton}
|
||||||
|
onPress={muteOthersPress}
|
||||||
|
>
|
||||||
|
<CompassIcon
|
||||||
|
size={16}
|
||||||
|
name={'microphone-off'}
|
||||||
|
style={styles.muteIcon}
|
||||||
|
/>
|
||||||
|
<Text style={styles.muteText}>
|
||||||
|
{intl.formatMessage({id: 'mobile.calls_mute_others', defaultMessage: 'Mute others'})}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
</View>
|
</View>
|
||||||
<List
|
<List
|
||||||
data={sessions}
|
data={sessions}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const EditProfilePicture = ({user, onUpdateProfilePicture}: ChangeProfilePicture
|
||||||
let prefix = '';
|
let prefix = '';
|
||||||
if (pictureUrl.includes('/api/')) {
|
if (pictureUrl.includes('/api/')) {
|
||||||
prefix = serverUrl;
|
prefix = serverUrl;
|
||||||
} else if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') && !pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://')) {
|
} else if (Platform.OS === 'android' && !pictureUrl.startsWith('content://') && !pictureUrl.startsWith('http://') && !pictureUrl.startsWith('https://') && !pictureUrl.startsWith('file://')) {
|
||||||
prefix = 'file://';
|
prefix = 'file://';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,7 @@
|
||||||
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
|
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
|
||||||
"mobile.calls_more": "More",
|
"mobile.calls_more": "More",
|
||||||
"mobile.calls_mute": "Mute",
|
"mobile.calls_mute": "Mute",
|
||||||
|
"mobile.calls_mute_others": "Mute others",
|
||||||
"mobile.calls_mute_participant": "Mute participant",
|
"mobile.calls_mute_participant": "Mute participant",
|
||||||
"mobile.calls_name_is_talking_postfix": "is talking...",
|
"mobile.calls_name_is_talking_postfix": "is talking...",
|
||||||
"mobile.calls_name_started_call": "{name} started a call",
|
"mobile.calls_name_started_call": "{name} started a call",
|
||||||
|
|
@ -515,6 +516,12 @@
|
||||||
"mobile.calls_recording_start_no_permissions": "You don't have permissions to start a recording. Please ask the call host to start a recording.",
|
"mobile.calls_recording_start_no_permissions": "You don't have permissions to start a recording. Please ask the call host to start a recording.",
|
||||||
"mobile.calls_recording_stop_no_permissions": "You don't have permissions to stop the recording. Please ask the call host to stop the recording.",
|
"mobile.calls_recording_stop_no_permissions": "You don't have permissions to stop the recording. Please ask the call host to stop the recording.",
|
||||||
"mobile.calls_recording_stop_none_in_progress": "No recording is in progress.",
|
"mobile.calls_recording_stop_none_in_progress": "No recording is in progress.",
|
||||||
|
"mobile.calls_remove": "Remove",
|
||||||
|
"mobile.calls_remove_alert_body": "Are you sure you want to remove {displayName} from the call? ",
|
||||||
|
"mobile.calls_remove_alert_title": "Remove participant",
|
||||||
|
"mobile.calls_remove_participant": "Remove from call",
|
||||||
|
"mobile.calls_removed_alert_body": "The host removed you from the call.",
|
||||||
|
"mobile.calls_removed_alert_title": "You were removed from the call",
|
||||||
"mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance",
|
"mobile.calls_request_message": "Calls are currently running in test mode and only system admins can start them. Reach out directly to your system admin for assistance",
|
||||||
"mobile.calls_request_title": "Calls is not currently enabled",
|
"mobile.calls_request_title": "Calls is not currently enabled",
|
||||||
"mobile.calls_see_logs": "See server logs",
|
"mobile.calls_see_logs": "See server logs",
|
||||||
|
|
@ -531,6 +538,7 @@
|
||||||
"mobile.calls_user_left_channel_error_title": "You left the channel",
|
"mobile.calls_user_left_channel_error_title": "You left the channel",
|
||||||
"mobile.calls_user_removed_from_channel_error_message": "You have been removed from the channel, and have been disconnected from the call.",
|
"mobile.calls_user_removed_from_channel_error_message": "You have been removed from the channel, and have been disconnected from the call.",
|
||||||
"mobile.calls_user_removed_from_channel_error_title": "You were removed from channel",
|
"mobile.calls_user_removed_from_channel_error_title": "You were removed from channel",
|
||||||
|
"mobile.calls_view_profile": "View profile",
|
||||||
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
|
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
|
||||||
"mobile.calls_you": "(you)",
|
"mobile.calls_you": "(you)",
|
||||||
"mobile.calls_you_2": "You",
|
"mobile.calls_you_2": "You",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Requires Mattermost Server v8.1.0+. Older servers may not be able to connect or have unexpected behavior.
|
Requires Mattermost Server v9.5.0+. Older servers may not be able to connect or have unexpected behavior.
|
||||||
|
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
This version is compatible with Mattermost servers v8.1.0+.
|
This version is compatible with Mattermost servers v9.5.0+.
|
||||||
|
|
||||||
Please see [changelog](https://docs.mattermost.com/administration/mobile-changelog.html) for full release notes. If you're interested in helping beta test upcoming versions before they are released, please see our [documentation](https://github.com/mattermost/mattermost-mobile#testing).
|
Please see [changelog](https://docs.mattermost.com/administration/mobile-changelog.html) for full release notes. If you're interested in helping beta test upcoming versions before they are released, please see our [documentation](https://github.com/mattermost/mattermost-mobile#testing).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1945,7 +1945,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 520;
|
CURRENT_PROJECT_VERSION = 529;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
|
|
@ -1989,7 +1989,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 520;
|
CURRENT_PROJECT_VERSION = 529;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
HEADER_SEARCH_PATHS = (
|
HEADER_SEARCH_PATHS = (
|
||||||
|
|
@ -2132,7 +2132,7 @@
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 520;
|
CURRENT_PROJECT_VERSION = 529;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
|
@ -2181,7 +2181,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 520;
|
CURRENT_PROJECT_VERSION = 529;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.16.0</string>
|
<string>2.17.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>520</string>
|
<string>529</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.16.0</string>
|
<string>2.17.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>520</string>
|
<string>529</string>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
<array>
|
<array>
|
||||||
<string>OpenSans-Bold.ttf</string>
|
<string>OpenSans-Bold.ttf</string>
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XPC!</string>
|
<string>XPC!</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.16.0</string>
|
<string>2.17.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>520</string>
|
<string>529</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.16.0",
|
"version": "2.17.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.16.0",
|
"version": "2.17.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache 2.0",
|
"license": "Apache 2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mattermost-mobile",
|
"name": "mattermost-mobile",
|
||||||
"version": "2.16.0",
|
"version": "2.17.1",
|
||||||
"description": "Mattermost Mobile with React Native",
|
"description": "Mattermost Mobile with React Native",
|
||||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||||
"author": "Mattermost, Inc.",
|
"author": "Mattermost, Inc.",
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ index 027c366..5807e79 100644
|
||||||
unsafeExecute(work: UnsafeExecuteOperations, callback: ResultCallback<void>): void
|
unsafeExecute(work: UnsafeExecuteOperations, callback: ResultCallback<void>): void
|
||||||
|
|
||||||
diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
||||||
index 2f170e0..bac0352 100644
|
index 2f170e0..bd87f92 100644
|
||||||
--- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
--- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
||||||
+++ b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
+++ b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabase.java
|
||||||
@@ -11,6 +11,8 @@ import java.util.Arrays;
|
@@ -11,6 +11,8 @@ import java.util.Arrays;
|
||||||
|
|
@ -414,17 +414,19 @@ index 2f170e0..bac0352 100644
|
||||||
} else {
|
} else {
|
||||||
// On some systems there is some kind of lock on `/databases` folder ¯\_(ツ)_/¯
|
// On some systems there is some kind of lock on `/databases` folder ¯\_(ツ)_/¯
|
||||||
path = context.getDatabasePath("" + name + ".db").getPath().replace("/databases", "");
|
path = context.getDatabasePath("" + name + ".db").getPath().replace("/databases", "");
|
||||||
@@ -172,6 +190,10 @@ public class WMDatabase {
|
@@ -172,7 +190,11 @@ public class WMDatabase {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- interface TransactionFunction {
|
||||||
+ public void unsafeVacuum() {
|
+ public void unsafeVacuum() {
|
||||||
+ execute("vacuum");
|
+ execute("vacuum");
|
||||||
+ }
|
+ }
|
||||||
+
|
+
|
||||||
interface TransactionFunction {
|
+ public interface TransactionFunction {
|
||||||
void applyTransactionFunction();
|
void applyTransactionFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java
|
diff --git a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java b/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java
|
||||||
index 117b2bc..57e4abb 100644
|
index 117b2bc..57e4abb 100644
|
||||||
--- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java
|
--- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue