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
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (8.1.0+)
|
||||
- **Minimum Server versions:** Current ESR version (9.5.0+)
|
||||
- **Supported iOS versions:** 12.4+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ android {
|
|||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 520
|
||||
versionName "2.16.0"
|
||||
versionCode 529
|
||||
versionName "2.17.1"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,27 @@ 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 kotlinx.coroutines.*
|
||||
import com.mattermost.helpers.database_extension.getDatabaseForServer
|
||||
import com.mattermost.helpers.database_extension.saveToDatabase
|
||||
import com.mattermost.helpers.push_notification.addToDefaultCategoryIfNeeded
|
||||
import com.mattermost.helpers.push_notification.fetchMyChannel
|
||||
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.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
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)
|
||||
suspend fun fetchAndStoreDataForPushNotification(initialData: Bundle, isReactInit: Boolean): Bundle? {
|
||||
return withContext(Dispatchers.Default) {
|
||||
PushNotificationDataRunnable.start(context, initialData, isReactInit)
|
||||
}
|
||||
runBlocking {
|
||||
job.join()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +35,8 @@ class PushNotificationDataRunnable {
|
|||
private val mutex = Mutex()
|
||||
|
||||
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 {
|
||||
// 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 db = dbHelper.getDatabaseForServer(context, serverUrl)
|
||||
var result: Bundle? = null
|
||||
|
|
@ -50,8 +48,9 @@ class PushNotificationDataRunnable {
|
|||
val postId = initialData.getString("post_id")
|
||||
val rootId = initialData.getString("root_id")
|
||||
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 notificationData = Arguments.createMap()
|
||||
|
|
@ -89,7 +88,7 @@ class PushNotificationDataRunnable {
|
|||
|
||||
getThreadList(notificationThread, postData?.getArray("threads"))?.let {
|
||||
val threadsArray = Arguments.createArray()
|
||||
for(item in it) {
|
||||
for (item in it) {
|
||||
threadsArray.pushMap(item)
|
||||
}
|
||||
notificationData.putArray("threads", threadsArray)
|
||||
|
|
@ -105,7 +104,7 @@ class PushNotificationDataRunnable {
|
|||
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) {
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.mattermost.helpers.database_extension
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.text.TextUtils
|
||||
import com.facebook.react.bridge.Arguments
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
|
@ -8,8 +9,7 @@ import com.mattermost.helpers.DatabaseHelper
|
|||
import com.mattermost.helpers.QueryArgs
|
||||
import com.mattermost.helpers.mapCursor
|
||||
import com.nozbe.watermelondb.WMDatabase
|
||||
import java.util.*
|
||||
import kotlin.Exception
|
||||
import java.util.Arrays
|
||||
|
||||
internal fun DatabaseHelper.saveToDatabase(db: WMDatabase, data: ReadableMap, teamId: String?, channelId: String?, receivingThreads: Boolean) {
|
||||
db.transaction {
|
||||
|
|
@ -57,7 +57,7 @@ fun DatabaseHelper.getDatabaseForServer(context: Context?, serverUrl: String): W
|
|||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
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) {
|
||||
|
|
@ -73,7 +73,7 @@ fun DatabaseHelper.getDeviceToken(): String? {
|
|||
defaultDatabase!!.rawQuery(query, arrayOf("deviceToken")).use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(0);
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} 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?
|
||||
): IPushNotification {
|
||||
return CustomPushNotification(
|
||||
context,
|
||||
bundle,
|
||||
defaultFacade,
|
||||
defaultAppLaunchHelper,
|
||||
context!!,
|
||||
bundle!!,
|
||||
defaultFacade!!,
|
||||
defaultAppLaunchHelper!!,
|
||||
JsIOHelper()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
handleCallUserVoiceOn,
|
||||
handleHostLowerHand,
|
||||
handleHostMute,
|
||||
handleHostRemoved,
|
||||
handleUserDismissedNotification,
|
||||
} from '@calls/connection/websocket_event_handlers';
|
||||
import {isSupportedServerCalls} from '@calls/utils';
|
||||
|
|
@ -461,6 +462,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
|||
case WebsocketEvents.CALLS_HOST_LOWER_HAND:
|
||||
handleHostLowerHand(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_HOST_REMOVED:
|
||||
handleHostRemoved(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ const AtMention = ({
|
|||
const filteredUsers = filterResults(fallbackUsers, term);
|
||||
setFilteredLocalUsers(filteredUsers.length ? filteredUsers : emptyUserlList);
|
||||
} else if (receivedUsers) {
|
||||
sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
|
||||
await sortRecievedUsers(sUrl, term, receivedUsers?.users, receivedUsers?.out_of_channel);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
|
@ -298,8 +298,14 @@ const AtMention = ({
|
|||
|
||||
if (outOfChannel?.length) {
|
||||
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);
|
||||
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)) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {ACCOUNT_OUTLINE_IMAGE} from '@constants/profile';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getLastPictureUpdate} from '@utils/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;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const lastPictureUpdateAt = author ? getLastPictureUpdate(author) : 0;
|
||||
const fIStyle = useMemo(() => ({
|
||||
borderRadius: size / 2,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
|
|
@ -56,7 +58,7 @@ const Image = ({author, forwardRef, iconSize, size, source, url}: Props) => {
|
|||
|
||||
const pictureUrl = buildProfileImageUrlFromUser(serverUrl, author);
|
||||
return source ?? {uri: buildAbsoluteUrl(serverUrl, pictureUrl)};
|
||||
}, [author, serverUrl, source]);
|
||||
}, [author, serverUrl, source, lastPictureUpdateAt]);
|
||||
|
||||
if (typeof source === 'string') {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ const WebsocketEvents = {
|
|||
CALLS_CAPTION: `custom_${Calls.PluginId}_caption`,
|
||||
CALLS_HOST_MUTE: `custom_${Calls.PluginId}_host_mute`,
|
||||
CALLS_HOST_LOWER_HAND: `custom_${Calls.PluginId}_host_lower_hand`,
|
||||
CALLS_HOST_REMOVED: `custom_${Calls.PluginId}_host_removed`,
|
||||
|
||||
GROUP_RECEIVED: 'received_group',
|
||||
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) => {
|
||||
try {
|
||||
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 {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 {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||
import {hostRemovedErr, userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
|
|
@ -19,6 +19,7 @@ import {errorAlert} from '@calls/utils';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
|
|
@ -413,8 +414,55 @@ export const showErrorAlertOnClose = (err: Error, intl: IntlShape) => {
|
|||
}),
|
||||
);
|
||||
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:
|
||||
// Fallback with generic error
|
||||
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>;
|
||||
hostMake: (callId: string, newHostId: string) => Promise<ApiResp>;
|
||||
hostMute: (callId: string, sessionId: string) => Promise<ApiResp>;
|
||||
hostMuteOthers: (callId: string) => Promise<ApiResp>;
|
||||
hostScreenOff: (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 {
|
||||
|
|
@ -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) => {
|
||||
return this.doFetch(
|
||||
`${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;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
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 {
|
||||
callEnded,
|
||||
callStarted,
|
||||
|
|
@ -228,3 +229,14 @@ export const handleHostLowerHand = async (serverUrl: string, msg: WebSocketMessa
|
|||
|
||||
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 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 {useIntl} from 'react-intl';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {Platform, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
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 {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import BottomSheet from '@screens/bottom_sheet';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import UserProfileTitle from '@screens/user_profile/title';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
import type {CallSession, CurrentCall} from '@calls/types/calls';
|
||||
|
||||
const TITLE_HEIGHT = 118;
|
||||
const ITEM_HEIGHT = 48;
|
||||
const SEPARATOR_HEIGHT = 17;
|
||||
const ANDROID_BUMP_HEIGHT = 20;
|
||||
|
||||
type Props = {
|
||||
currentCall: CurrentCall;
|
||||
|
|
@ -27,11 +34,18 @@ type Props = {
|
|||
hideGuestTags: boolean;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
iconStyle: {
|
||||
marginRight: 6,
|
||||
},
|
||||
});
|
||||
separator: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
width: '100%',
|
||||
alignSelf: 'center',
|
||||
marginVertical: 8,
|
||||
},
|
||||
}));
|
||||
|
||||
export const HostControls = ({
|
||||
currentCall,
|
||||
|
|
@ -41,7 +55,10 @@ export const HostControls = ({
|
|||
hideGuestTags,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const {openUserProfile} = useHostMenus();
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
const sharingScreen = currentCall.screenOn === session.sessionId;
|
||||
|
||||
|
|
@ -63,20 +80,35 @@ export const HostControls = ({
|
|||
const stopScreensharePress = useCallback(async () => {
|
||||
hostStopScreenshare(currentCall.serverUrl, currentCall.channelId, session.sessionId);
|
||||
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 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 [
|
||||
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]);
|
||||
|
||||
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 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 = () => {
|
||||
if (!session?.userModel) {
|
||||
|
|
@ -125,6 +157,20 @@ export const HostControls = ({
|
|||
onPress={makeHostPress}
|
||||
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 {ParticipantsList} from '@calls/screens/participants_list/participants_list';
|
||||
import {observeCurrentCall} from '@calls/state';
|
||||
import {observeTeammateNameDisplay} from '@queries/servers/user';
|
||||
|
||||
const enhanced = withObservables([], () => {
|
||||
|
|
@ -14,10 +15,22 @@ const enhanced = withObservables([], () => {
|
|||
switchMap((db) => (db ? observeTeammateNameDisplay(db) : of$(''))),
|
||||
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 {
|
||||
sessionsDict: observeCurrentSessionsDict(),
|
||||
teammateNameDisplay,
|
||||
callServerUrl,
|
||||
callChannelId,
|
||||
callUserId,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,16 @@
|
|||
import {BottomSheetFlatList} from '@gorhom/bottom-sheet';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
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 {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 Pill from '@calls/screens/participants_list/pill';
|
||||
import {sortSessions} from '@calls/utils';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
|
|
@ -31,11 +33,15 @@ type Props = {
|
|||
closeButtonId: string;
|
||||
sessionsDict: Dictionary<CallSession>;
|
||||
teammateNameDisplay: string;
|
||||
callServerUrl?: string;
|
||||
callChannelId?: string;
|
||||
callUserId?: string;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
header: {
|
||||
paddingBottom: 12,
|
||||
paddingRight: 2,
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
|
|
@ -44,12 +50,35 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
color: theme.centerChannelColor,
|
||||
...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 theme = useTheme();
|
||||
const {onPress} = useHostMenus();
|
||||
const hostControlsAvailable = useHostControlsAvailable();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const {height} = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -63,6 +92,14 @@ export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDispl
|
|||
if (sessions.length > MIN_ROWS && snapPoint1 < 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>) => (
|
||||
<Participant
|
||||
|
|
@ -83,6 +120,21 @@ export const ParticipantsList = ({closeButtonId, sessionsDict, teammateNameDispl
|
|||
defaultMessage={'Participants'}
|
||||
/>
|
||||
<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>
|
||||
<List
|
||||
data={sessions}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const EditProfilePicture = ({user, onUpdateProfilePicture}: ChangeProfilePicture
|
|||
let prefix = '';
|
||||
if (pictureUrl.includes('/api/')) {
|
||||
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://';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -486,6 +486,7 @@
|
|||
"mobile.calls_mic_error": "To participate, open Settings to grant Mattermost access to your microphone.",
|
||||
"mobile.calls_more": "More",
|
||||
"mobile.calls_mute": "Mute",
|
||||
"mobile.calls_mute_others": "Mute others",
|
||||
"mobile.calls_mute_participant": "Mute participant",
|
||||
"mobile.calls_name_is_talking_postfix": "is talking...",
|
||||
"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_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_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_title": "Calls is not currently enabled",
|
||||
"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_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_view_profile": "View profile",
|
||||
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
|
||||
"mobile.calls_you": "(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).
|
||||
|
||||
|
|
|
|||
|
|
@ -1945,7 +1945,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 520;
|
||||
CURRENT_PROJECT_VERSION = 529;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
|
|
@ -1989,7 +1989,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 520;
|
||||
CURRENT_PROJECT_VERSION = 529;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
ENABLE_BITCODE = NO;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
|
|
@ -2132,7 +2132,7 @@
|
|||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 520;
|
||||
CURRENT_PROJECT_VERSION = 529;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
@ -2181,7 +2181,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 520;
|
||||
CURRENT_PROJECT_VERSION = 529;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.16.0</string>
|
||||
<string>2.17.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>520</string>
|
||||
<string>529</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.16.0</string>
|
||||
<string>2.17.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>520</string>
|
||||
<string>529</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.16.0</string>
|
||||
<string>2.17.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>520</string>
|
||||
<string>529</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.16.0",
|
||||
"version": "2.17.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.16.0",
|
||||
"version": "2.17.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache 2.0",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mattermost-mobile",
|
||||
"version": "2.16.0",
|
||||
"version": "2.17.1",
|
||||
"description": "Mattermost Mobile with React Native",
|
||||
"repository": "git@github.com:mattermost/mattermost-mobile.git",
|
||||
"author": "Mattermost, Inc.",
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ index 027c366..5807e79 100644
|
|||
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
|
||||
index 2f170e0..bac0352 100644
|
||||
index 2f170e0..bd87f92 100644
|
||||
--- 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
|
||||
@@ -11,6 +11,8 @@ import java.util.Arrays;
|
||||
|
|
@ -414,17 +414,19 @@ index 2f170e0..bac0352 100644
|
|||
} else {
|
||||
// On some systems there is some kind of lock on `/databases` folder ¯\_(ツ)_/¯
|
||||
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() {
|
||||
+ execute("vacuum");
|
||||
+ }
|
||||
+
|
||||
interface TransactionFunction {
|
||||
+ public interface TransactionFunction {
|
||||
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
|
||||
index 117b2bc..57e4abb 100644
|
||||
--- a/node_modules/@nozbe/watermelondb/native/android/src/main/java/com/nozbe/watermelondb/WMDatabaseBridge.java
|
||||
|
|
|
|||
Loading…
Reference in a new issue