Merge pull request #840 from session-foundation/release/1.20.8

Merge Release/1.20.8 back into master
pull/1709/head
SessionHero01 4 months ago committed by GitHub
commit a99e27dda0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -13,8 +13,8 @@ configurations.forEach {
it.exclude module: "commons-logging"
}
def canonicalVersionCode = 389
def canonicalVersionName = "1.20.7"
def canonicalVersionCode = 390
def canonicalVersionName = "1.20.8"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
@ -299,7 +299,7 @@ dependencies {
implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation 'io.github.webrtc-sdk:android:125.6422.04'
implementation 'io.github.webrtc-sdk:android:125.6422.06.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'

@ -29,38 +29,49 @@
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> <!-- For video calls -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <!-- For calls that get audio from bluetooth headsets -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- Google may (potentially) insist we implement `ConnectionService` to request MANAGE_OWN_CALLS - see:
- https://developer.android.com/reference/android/Manifest.permission#MANAGE_OWN_CALLS
- https://developer.android.com/reference/android/telecom/ConnectionService
-->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.app.role.DIALER" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" tools:node="remove"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<queries>
<intent>
@ -304,8 +315,10 @@
</activity>
<activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:foregroundServiceType="microphone"
<service
android:enabled="true"
android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:foregroundServiceType="phoneCall"
android:exported="false" />
<service
android:name="org.thoughtcrime.securesms.service.KeyCachingService"

@ -20,12 +20,13 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
@ -33,7 +34,20 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
import org.conscrypt.Conscrypt;
import org.session.libsession.database.MessageDataProvider;
import org.session.libsession.messaging.MessagingModuleConfiguration;
@ -44,6 +58,7 @@ import org.session.libsession.snode.SnodeModule;
import org.session.libsession.utilities.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.Environment;
import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences;
@ -88,25 +103,8 @@ import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp;
import network.loki.messenger.BuildConfig;
import network.loki.messenger.R;
import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile;
import org.webrtc.PeerConnectionFactory;
/**
* Will be called once when the TextSecure process is created.
@ -148,7 +146,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration;
private volatile boolean isAppVisible;
public volatile boolean isAppVisible;
public String KEYGUARD_LOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":KeyguardLock";
public String WAKELOCK_TAG = NonTranslatableStringConstants.APP_NAME + ":WakeLock";
@Override
public Object getSystemService(String name) {
@ -457,11 +457,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
// Method to clear the local data - returns true on success otherwise false
/**
* Clear all local profile data and message history.
* @return true on success, false otherwise.
*/
@SuppressLint("ApplySharedPref")
public boolean clearAllData() {
TextSecurePreferences.clearAll(this);
@ -492,4 +487,35 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
}
// endregion
// Method to wake up the screen and dismiss the keyguard
public void wakeUpDeviceAndDismissKeyguardIfRequired() {
// Get the KeyguardManager and PowerManager
KeyguardManager keyguardManager = (KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
// Check if the phone is locked & if the screen is awake
boolean isPhoneLocked = keyguardManager.isKeyguardLocked();
boolean isScreenAwake = powerManager.isInteractive();
if (!isScreenAwake) {
PowerManager.WakeLock wakeLock = powerManager.newWakeLock(
PowerManager.FULL_WAKE_LOCK
| PowerManager.ACQUIRE_CAUSES_WAKEUP
| PowerManager.ON_AFTER_RELEASE,
WAKELOCK_TAG);
// Acquire the wake lock to wake up the device
wakeLock.acquire(3000);
}
// Dismiss the keyguard.
// Note: This will not bypass any app-level (Session) lock; only the device-level keyguard.
// TODO: When moving to a minimum Android API of 27, replace these deprecated calls with new APIs.
if (isPhoneLocked) {
KeyguardManager.KeyguardLock keyguardLock = keyguardManager.newKeyguardLock(KEYGUARD_LOCK_TAG);
keyguardLock.disableKeyguard();
}
}
}

@ -21,9 +21,9 @@ import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import java.util.List;
import java.util.Objects;
import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.messaging.utilities.UpdateMessageBuilder;
@ -33,9 +33,6 @@ import org.session.libsession.utilities.NetworkFailure;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.util.List;
import java.util.Objects;
/**
* The base class for message record models that are displayed in
* conversations, as opposed to models that are displayed in a thread list.

@ -103,7 +103,7 @@ public class ThreadRecord extends DisplayRecord {
@Override
public CharSequence getDisplayBody(@NonNull Context context) {
// no need to display anything if there are no messages
if(lastMessage == null){
if (lastMessage == null){
return "";
}
else if (isGroupUpdateMessage()) {

@ -28,8 +28,6 @@ import android.database.Cursor
import android.os.AsyncTask
import android.os.Build
import android.text.TextUtils
import android.widget.Toast
import androidx.camera.core.impl.utils.ContextUtil.getApplicationContext
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -47,8 +45,8 @@ import org.session.libsession.messaging.utilities.AccountId
import org.session.libsession.messaging.utilities.SodiumUtilities.blindedKeyPair
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.ServiceUtil
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY
import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY
import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber
import org.session.libsession.utilities.TextSecurePreferences.Companion.getNotificationPrivacy
import org.session.libsession.utilities.TextSecurePreferences.Companion.getRepeatAlertsCount
@ -70,8 +68,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.ShareLogsDialog
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification
import org.thoughtcrime.securesms.util.SpanUtil
@ -202,7 +198,6 @@ class DefaultMessageNotifier : MessageNotifier {
override fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) {
var playNotificationAudio = signal // Local copy of the argument so we can modify it
var telcoCursor: Cursor? = null
val pushCursor: Cursor? = null
try {
telcoCursor = get(context).mmsSmsDatabase().unread // TODO: add a notification specific lighter query here
@ -228,14 +223,14 @@ class DefaultMessageNotifier : MessageNotifier {
sendSingleThreadNotification(context, NotificationState(notificationState.getNotificationsForThread(threadId)), false, true)
}
sendMultipleThreadNotification(context, notificationState, playNotificationAudio)
} else if (notificationState.messageCount > 0) {
} else if (notificationState.notificationCount > 0) {
sendSingleThreadNotification(context, notificationState, playNotificationAudio, false)
} else {
cancelActiveNotifications(context)
}
cancelOrphanedNotifications(context, notificationState)
updateBadge(context, notificationState.messageCount)
updateBadge(context, notificationState.notificationCount)
if (playNotificationAudio) {
scheduleReminder(context, reminderCount)
@ -267,7 +262,7 @@ class DefaultMessageNotifier : MessageNotifier {
val builder = SingleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications
val recipient = notifications[0].recipient
val messageOriginator = notifications[0].recipient
val notificationId = (SUMMARY_NOTIFICATION_ID + (if (bundled) notifications[0].threadId else 0)).toInt()
val messageIdTag = notifications[0].timestamp.toString()
@ -285,12 +280,12 @@ class DefaultMessageNotifier : MessageNotifier {
builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag)
val text = notifications[0].text
val notificationText = notifications[0].text
builder.setThread(notifications[0].recipient)
builder.setMessageCount(notificationState.messageCount)
builder.setMessageCount(notificationState.notificationCount)
val builderCS = text ?: ""
val builderCS = notificationText ?: ""
val ss = highlightMentions(
builderCS,
false,
@ -301,7 +296,7 @@ class DefaultMessageNotifier : MessageNotifier {
)
builder.setPrimaryMessageBody(
recipient,
messageOriginator,
notifications[0].individualRecipient,
ss,
notifications[0].slideDeck
@ -313,12 +308,12 @@ class DefaultMessageNotifier : MessageNotifier {
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
builder.setAutoCancel(true)
val replyMethod = ReplyMethod.forRecipient(context, recipient)
val replyMethod = ReplyMethod.forRecipient(context, messageOriginator)
val canReply = canUserReplyToNotification(recipient)
val canReply = canUserReplyToNotification(messageOriginator)
val quickReplyIntent = if (canReply) notificationState.getQuickReplyIntent(context, recipient) else null
val remoteReplyIntent = if (canReply) notificationState.getRemoteReplyIntent(context, recipient, replyMethod) else null
val quickReplyIntent = if (canReply) notificationState.getQuickReplyIntent(context, messageOriginator) else null
val remoteReplyIntent = if (canReply) notificationState.getRemoteReplyIntent(context, messageOriginator, replyMethod) else null
builder.addActions(
notificationState.getMarkAsReadIntent(context, notificationId),
@ -329,14 +324,13 @@ class DefaultMessageNotifier : MessageNotifier {
if (canReply) {
builder.addAndroidAutoAction(
notificationState.getAndroidAutoReplyIntent(context, recipient),
notificationState.getAndroidAutoReplyIntent(context, messageOriginator),
notificationState.getAndroidAutoHeardIntent(context, notificationId),
notifications[0].timestamp
)
}
val iterator: ListIterator<NotificationItem> = notifications.listIterator(notifications.size)
while (iterator.hasPrevious()) {
val item = iterator.previous()
builder.addMessageBody(item.recipient, item.individualRecipient, item.text)
@ -368,6 +362,7 @@ class DefaultMessageNotifier : MessageNotifier {
// for ActivityCompat#requestPermissions for more details.
return
}
NotificationManagerCompat.from(context).notify(notificationId, notification)
Log.i(TAG, "Posted notification. $notification")
}
@ -383,7 +378,7 @@ class DefaultMessageNotifier : MessageNotifier {
val builder = MultipleRecipientNotificationBuilder(context, getNotificationPrivacy(context))
val notifications = notificationState.notifications
builder.setMessageCount(notificationState.messageCount, notificationState.threadCount)
builder.setMessageCount(notificationState.notificationCount, notificationState.threadCount)
builder.setMostRecentSender(notifications[0].individualRecipient, notifications[0].recipient)
builder.setGroup(NOTIFICATION_GROUP)
builder.setDeleteIntent(notificationState.getDeleteIntent(context))

@ -3,20 +3,18 @@ package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.Recipient.VibrateState;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import org.session.libsession.utilities.recipients.Recipient.VibrateState;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
public class NotificationState {
@ -36,68 +34,47 @@ public class NotificationState {
}
public void addNotification(NotificationItem item) {
// Add this new notification at the beginning of the list
notifications.addFirst(item);
if (threads.contains(item.getThreadId())) {
threads.remove(item.getThreadId());
}
// Put a notification at the front by removing it then re-adding it?
threads.remove(item.getThreadId());
threads.add(item.getThreadId());
notificationCount++;
}
public @Nullable Uri getRingtone(@NonNull Context context) {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient();
if (recipient != null) {
return NotificationChannels.getMessageRingtone(context, recipient);
}
return NotificationChannels.getMessageRingtone(context, recipient);
}
return null;
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
}
public VibrateState getVibrate() {
if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient();
if (recipient != null) {
return recipient.resolve().getMessageVibrate();
}
return recipient.resolve().getMessageVibrate();
}
return VibrateState.DEFAULT;
}
public boolean hasMultipleThreads() {
return threads.size() > 1;
}
public LinkedHashSet<Long> getThreads() {
return threads;
}
public int getThreadCount() {
return threads.size();
}
public int getMessageCount() {
return notificationCount;
}
public List<NotificationItem> getNotifications() {
return notifications;
}
public boolean hasMultipleThreads() { return threads.size() > 1; }
public LinkedHashSet<Long> getThreads() { return threads; }
public int getThreadCount() { return threads.size(); }
public int getNotificationCount() { return notificationCount; }
public List<NotificationItem> getNotifications() { return notifications; }
public List<NotificationItem> getNotificationsForThread(long threadId) {
LinkedList<NotificationItem> list = new LinkedList<>();
LinkedList<NotificationItem> notificationsInThread = new LinkedList<>();
for (NotificationItem item : notifications) {
if (item.getThreadId() == threadId) list.addFirst(item);
if (item.getThreadId() == threadId) notificationsInThread.addFirst(item);
}
return list;
return notificationsInThread;
}
public PendingIntent getMarkAsReadIntent(Context context, int notificationId) {
@ -111,7 +88,7 @@ public class NotificationState {
Intent intent = new Intent(MarkReadReceiver.CLEAR_ACTION);
intent.setClass(context, MarkReadReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
intent.putExtra(MarkReadReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(MarkReadReceiver.NOTIFICATION_ID_EXTRA, notificationId);
@ -171,7 +148,7 @@ public class NotificationState {
Intent intent = new Intent(AndroidAutoHeardReceiver.HEARD_ACTION);
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setClass(context, AndroidAutoHeardReceiver.class);
intent.setData((Uri.parse("custom://"+System.currentTimeMillis())));
intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
intent.putExtra(AndroidAutoHeardReceiver.THREAD_IDS_EXTRA, threadArray);
intent.putExtra(AndroidAutoHeardReceiver.NOTIFICATION_ID_EXTRA, notificationId);
intent.setPackage(context.getPackageName());
@ -223,6 +200,4 @@ public class NotificationState {
return PendingIntent.getBroadcast(context, 0, intent, intentFlags);
}
}
}

@ -1,5 +0,0 @@
package org.thoughtcrime.securesms.notifications
interface PushManager {
fun refresh(force: Boolean)
}

@ -248,8 +248,6 @@ public class KeyCachingService extends Service {
.put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString();
builder.setContentTitle(unlockedTxt);
builder.setContentText(getString(R.string.lockAppUnlock));
builder.setSmallIcon(R.drawable.icon_cached);
builder.setWhen(0);
builder.setPriority(Notification.PRIORITY_MIN);

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.IntentFilter
import android.content.pm.PackageManager
@ -19,11 +19,18 @@ import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.FutureTaskListener
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker
import org.thoughtcrime.securesms.util.CallNotificationBuilder
@ -32,6 +39,7 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallManager
import org.thoughtcrime.securesms.webrtc.CallViewModel
@ -44,6 +52,7 @@ import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.data.State as CallState
import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.DataChannel
import org.webrtc.IceCandidate
@ -54,13 +63,6 @@ import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
import org.webrtc.PeerConnection.IceConnectionState.FAILED
import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.thoughtcrime.securesms.webrtc.data.State as CallState
@AndroidEntryPoint
class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
@ -631,6 +633,20 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
}
/**
* Handles remote ICE candidates received from a signaling server.
*
* This function is called when a new ICE candidate is received for a specific call.
* It extracts the candidate information from the intent, creates IceCandidate objects,
* and passes them to the CallManager to be added to the PeerConnection.
*
* @param intent The intent containing the remote ICE candidate information.
* The intent should contain the following extras:
* - EXTRA_CALL_ID: The ID of the call.
* - EXTRA_ICE_SDP_MID: An array of SDP media stream identification strings.
* - EXTRA_ICE_SDP_LINE_INDEX: An array of SDP media line indexes.
* - EXTRA_ICE_SDP: An array of SDP candidate strings.
*/
private fun handleRemoteIceCandidate(intent: Intent) {
val callId = getCallId(intent)
val sdpMids = intent.getStringArrayExtra(EXTRA_ICE_SDP_MID) ?: return
@ -724,24 +740,40 @@ class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener {
}
}
// Over the course of setting up a phone call this method is called multiple times with `types`
// of PRE_OFFER -> RING_INCOMING -> ICE_MESSAGE
private fun setCallInProgressNotification(type: Int, recipient: Recipient?) {
try {
ServiceCompat.startForeground(
this,
CallNotificationBuilder.WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient),
if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else 0
)
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead", e)
// Wake the device if needed
(applicationContext as ApplicationContext).wakeUpDeviceAndDismissKeyguardIfRequired()
// If notifications are enabled we'll try and start a foreground service to show the notification
var failedToStartForegroundService = false
if (CallNotificationBuilder.areNotificationsEnabled(this)) {
try {
ServiceCompat.startForeground(
this,
WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient),
if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL else 0
)
return
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead", e)
failedToStartForegroundService = true
}
} else {
// Notifications are NOT enabled! Skipped attempt at startForeground and going straight to fullscreen intent attempt!
}
if (!CallNotificationBuilder.areNotificationsEnabled(this) && type == TYPE_INCOMING_PRE_OFFER) {
if ((type == TYPE_INCOMING_PRE_OFFER || type == TYPE_INCOMING_RINGING) && failedToStartForegroundService) {
// Start an intent for the fullscreen call activity
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java)
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_BROUGHT_TO_FRONT or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TOP)
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
startActivity(foregroundIntent)
return
}
}

@ -25,11 +25,11 @@ class CallNotificationBuilder {
companion object {
const val WEBRTC_NOTIFICATION = 313388
const val TYPE_INCOMING_RINGING = 1
const val TYPE_OUTGOING_RINGING = 2
const val TYPE_ESTABLISHED = 3
const val TYPE_INCOMING_RINGING = 1
const val TYPE_OUTGOING_RINGING = 2
const val TYPE_ESTABLISHED = 3
const val TYPE_INCOMING_CONNECTING = 4
const val TYPE_INCOMING_PRE_OFFER = 5
const val TYPE_INCOMING_PRE_OFFER = 5
@JvmStatic
fun areNotificationsEnabled(context: Context): Boolean {
@ -37,31 +37,6 @@ class CallNotificationBuilder {
return notificationManager.areNotificationsEnabled()
}
@JvmStatic
fun getFirstCallNotification(context: Context, callerName: String): Notification {
val contentIntent = Intent(context, SettingsActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val titleTxt = context.getSubbedString(R.string.callsMissedCallFrom, NAME_KEY to callerName)
val bodyTxt = context.getSubbedCharSequence(
R.string.callsYouMissedCallPermissions,
NAME_KEY to callerName
)
val builder = NotificationCompat.Builder(context, NotificationChannels.CALLS)
.setSound(null)
.setSmallIcon(R.drawable.ic_baseline_call_24)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(titleTxt)
.setContentText(bodyTxt)
.setStyle(NotificationCompat.BigTextStyle().bigText(bodyTxt))
.setAutoCancel(true)
return builder.build()
}
@JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
val contentIntent = Intent(context, WebRtcCallActivity::class.java)
@ -98,9 +73,7 @@ class CallNotificationBuilder {
R.string.decline
))
// If notifications aren't enabled, we will trigger the intent from WebRtcCallService
builder.setFullScreenIntent(getFullScreenPendingIntent(
context
), true)
builder.setFullScreenIntent(getFullScreenPendingIntent(context), true)
builder.addAction(getActivityNotificationAction(
context,
if (type == TYPE_INCOMING_PRE_OFFER) WebRtcCallActivity.ACTION_PRE_OFFER else WebRtcCallActivity.ACTION_ANSWER,
@ -143,9 +116,10 @@ class CallNotificationBuilder {
private fun getFullScreenPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, WebRtcCallActivity::class.java)
.setFlags(FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT)
// When launching the call activity do NOT keep it in the history when finished, as it does not pass through CALL_DISCONNECTED
// if the call was denied outright, and without this the "dead" activity will sit around in the history when the device is unlocked.
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
.setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.webrtc
import android.Manifest
import android.app.NotificationManager
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.os.PowerManager
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
@ -16,6 +17,7 @@ import org.session.libsession.messaging.messages.control.CallMessage
import org.session.libsession.messaging.utilities.WebRtcUtils
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.NonTranslatableStringConstants
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER
@ -25,27 +27,34 @@ import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PRE_OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.CallNotificationBuilder
import org.webrtc.IceCandidate
class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) {
companion object {
private const val TAG = "CallMessageProcessor"
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
fun safeStartService(context: Context, intent: Intent) {
// If the foreground service crashes then it's possible for one of these intents to
// be started in the background (in which case 'startService' will throw a
// 'BackgroundServiceStartNotAllowedException' exception) so catch that case and try
// to re-start the service in the foreground
try { context.startService(intent) }
catch(e: Exception) {
try { ContextCompat.startForegroundService(context, intent) }
catch (e2: Exception) {
Log.e("Loki", "Unable to start CallMessage intent: ${e2.message}")
fun safeStartForegroundService(context: Context, intent: Intent) {
// Wake up the device (if required) before attempting to start any services - otherwise on Android 12 and above we get
// a BackgroundServiceStartNotAllowedException such as:
// Unable to start CallMessage intent: startForegroundService() not allowed due to mAllowStartForeground false:
// service network.loki.messenger/org.thoughtcrime.securesms.service.WebRtcCallService
(context as ApplicationContext).wakeUpDeviceAndDismissKeyguardIfRequired()
// Attempt to start the call service..
try {
context.startService(intent)
} catch (e: Exception) {
Log.e("Loki", "Unable to start service: ${e.message}", e)
try {
ContextCompat.startForegroundService(context, intent)
} catch (e2: Exception) {
Log.e(TAG, "Unable to start CallMessage intent: ${e2.message}", e2)
}
}
}
@ -61,8 +70,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
Log.i("Loki", "Contact is approved?: $approvedContact")
if (!approvedContact && storage.getUserPublicKey() != sender) continue
// if the user has not enabled voice/video calls
// or if the user has not granted audio/microphone permissions
// If the user has not enabled voice/video calls or if the user has not granted audio/microphone permissions
if (
!textSecurePreferences.isCallNotificationsEnabled() ||
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO)
@ -101,21 +109,20 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
private fun incomingHangup(callMessage: CallMessage) {
val callId = callMessage.callId ?: return
val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId)
safeStartService(context, hangupIntent)
safeStartForegroundService(context, hangupIntent)
}
private fun incomingAnswer(callMessage: CallMessage) {
val recipientAddress = callMessage.sender ?: return
val callId = callMessage.callId ?: return
val sdp = callMessage.sdps.firstOrNull() ?: return
val recipientAddress = callMessage.sender ?: return Log.w(TAG, "Cannot answer incoming call without sender")
val callId = callMessage.callId ?: return Log.w(TAG, "Cannot answer incoming call without callId" )
val sdp = callMessage.sdps.firstOrNull() ?: return Log.w(TAG, "Cannot answer incoming call without sdp")
val answerIntent = WebRtcCallService.incomingAnswer(
context = context,
address = Address.fromSerialized(recipientAddress),
sdp = sdp,
callId = callId
)
safeStartService(context, answerIntent)
safeStartForegroundService(context, answerIntent)
}
private fun handleIceCandidates(callMessage: CallMessage) {
@ -131,7 +138,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId,
address = Address.fromSerialized(sender)
)
safeStartService(context, iceIntent)
safeStartForegroundService(context, iceIntent)
}
private fun incomingPreOffer(callMessage: CallMessage) {
@ -144,7 +151,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId,
callTime = callMessage.sentTimestamp!!
)
safeStartService(context, incomingIntent)
safeStartForegroundService(context, incomingIntent)
}
private fun incomingCall(callMessage: CallMessage) {
@ -158,7 +165,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId,
callTime = callMessage.sentTimestamp!!
)
safeStartService(context, incomingIntent)
safeStartForegroundService(context, incomingIntent)
}
private fun CallMessage.iceCandidates(): List<IceCandidate> {

@ -1 +1 @@
Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740
Subproject commit 43b1c6c341ee8739a8678c631d0713136dbfd05f

@ -9,6 +9,10 @@ import network.loki.messenger.libsession_util.Contacts
import network.loki.messenger.libsession_util.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
import kotlinx.coroutines.GlobalScope
import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
@ -28,9 +32,6 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
private const val TAG = "Poller"
@ -44,8 +45,9 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
// region Settings
companion object {
private const val retryInterval: Long = 2 * 1000
private const val maxInterval: Long = 15 * 1000
private const val RETRY_INTERVAL_MS: Long = 2 * 1000
private const val MAX_RETRY_INTERVAL_MS: Long = 15 * 1000
private const val NEXT_RETRY_MULTIPLIER: Float = 1.2f // If we fail to poll we multiply our current retry interval by this (up to the above max) then try again
}
// endregion
@ -54,7 +56,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
if (hasStarted) { return }
Log.d(TAG, "Started polling.")
hasStarted = true
setUpPolling(retryInterval)
setUpPolling(RETRY_INTERVAL_MS)
}
fun stopIfNeeded() {
@ -67,9 +69,11 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
Log.d(TAG, "Retrieving user profile.")
SnodeAPI.getSwarm(userPublicKey).bind {
usedSnodes.clear()
deferred<Unit, Exception>().also {
pollNextSnode(userProfileOnly = true, it)
deferred<Unit, Exception>().also { exception ->
pollNextSnode(userProfileOnly = true, exception)
}.promise
}.fail { exception ->
Log.e(TAG, "Failed to retrieve user profile.", exception)
}
}
// endregion
@ -84,14 +88,14 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
pollNextSnode(deferred = deferred)
deferred.promise
}.success {
val nextDelay = if (isCaughtUp) retryInterval else 0
val nextDelay = if (isCaughtUp) RETRY_INTERVAL_MS else 0
Timer().schedule(object : TimerTask() {
override fun run() {
thread.run { setUpPolling(retryInterval) }
thread.run { setUpPolling(RETRY_INTERVAL_MS) }
}
}, nextDelay)
}.fail {
val nextDelay = minOf(maxInterval, (delay * 1.2).toLong())
val nextDelay = minOf(MAX_RETRY_INTERVAL_MS, (delay * NEXT_RETRY_MULTIPLIER).toLong())
Timer().schedule(object : TimerTask() {
override fun run() {
thread.run { setUpPolling(nextDelay) }

@ -9,7 +9,7 @@
<string name="accountIdErrorInvalid">معرف الحساب هذا غير صالح. يرجى التحقق والمحاولة مرة أخرى.</string>
<string name="accountIdOrOnsEnter">أدخل معرف الحساب أو ONS</string>
<string name="accountIdOrOnsInvite">دعوة باستخدام معرف الحساب أو ONS</string>
<string name="accountIdShare">Hey, I\'ve been using {app_name} to chat with complete privacy and security. Come join me! My Account ID is\n\n{account_id}\n\nDownload it at {session_download_url}</string>
<string name="accountIdShare">مرحبًا، لقد كنت أستخدم {app_name} للدردشة مع خصوصية وأمان كاملين. انضم إليّ! معرف حسابي هو\n\n{account_id}\n\nقم بتحميله من {session_download_url}</string>
<string name="accountIdYours">معرف حسابك</string>
<string name="accountIdYoursDescription">هذا معرف الحساب الخاص بك. يمكن للمستخدمين الآخرين مسحه ضوئيا لبدء محادثة معك.</string>
<string name="actualSize">الحجم الحقيقي</string>
@ -53,7 +53,7 @@
<string name="appearanceThemesClassicLight">فاتح كلاسيكي</string>
<string name="appearanceThemesOceanDark">محيطي داكن</string>
<string name="appearanceThemesOceanLight">محيطي فاتح</string>
<string name="appearanceZoom">كبِر</string>
<string name="appearanceZoom">تكبير</string>
<string name="appearanceZoomIn">تكبير</string>
<string name="appearanceZoomOut">تصغير</string>
<string name="attachment">مرفق</string>
@ -68,7 +68,7 @@
<string name="attachmentsClickToDownload">اضغط لتنزيل {file_type}</string>
<string name="attachmentsCollapseOptions">إغلاق خيارات المرفق</string>
<string name="attachmentsCollecting">جارٍ جمع المرفقات...</string>
<string name="attachmentsDownload">نَزِل المرفق</string>
<string name="attachmentsDownload">تنزيل المرفق</string>
<string name="attachmentsDuration">المدة:</string>
<string name="attachmentsErrorLoad">خطأ في إرفاق الملف</string>
<string name="attachmentsErrorMediaSelection">فشل في تحديد المرفق</string>
@ -97,7 +97,7 @@
<string name="attachmentsNa">N/A</string>
<string name="attachmentsNotification">{emoji} مرفق</string>
<string name="attachmentsNotificationGroup">{author}: {emoji} مرفق</string>
<string name="attachmentsResolution">الدقة أو الأبعاد:</string>
<string name="attachmentsResolution">دقة الشاشة:</string>
<string name="attachmentsSaveError">تعذر حفظ الملف.</string>
<string name="attachmentsSendTo">إرسال إلى {name}</string>
<string name="attachmentsTapToDownload">انضغط لتنزيل {file_type}</string>
@ -107,7 +107,7 @@
<string name="audio">صوت</string>
<string name="audioNoInput">لا يوجد ميكروفون</string>
<string name="audioNoOutput">لا يوجد سماعات أو مكبر صوت</string>
<string name="audioUnableToPlay">غير قادر على تشغيل ملف الصوت.</string>
<string name="audioUnableToPlay">تعذّر تشغيل الملف الصوتي</string>
<string name="audioUnableToRecord">تعذر تسجيل الصوت.</string>
<string name="authenticateFailed">فشل في المصادقة</string>
<string name="authenticateFailedTooManyAttempts">عدد كبير جدًا من محاولات التحقق الفاشلة. يرجى المحاولة مرة أخرى لاحقًا.</string>
@ -118,11 +118,11 @@
<string name="banErrorFailed">فشل المنع</string>
<string name="banUnbanErrorFailed">لقد فشل الغاء المنع</string>
<string name="banUnbanUser">الغاء منع المستخدم</string>
<string name="banUnbanUserUnbanned">تم رفع الحظر عن المستخدم</string>
<string name="banUnbanUserUnbanned">تم رفع المنع عن المستخدم</string>
<string name="banUser">حظر المستخدم</string>
<string name="banUserBanned">تم حظر المستخدم</string>
<string name="banUserBanned">تم منع المستخدم</string>
<string name="block">حظر</string>
<string name="blockBlockedDescription">فك حظر هذه جهة الإتصال لإرسال رسالة</string>
<string name="blockBlockedDescription">إلغاء حظر جهة الإتصال لإرسال رسالة</string>
<string name="blockBlockedNone">لا توجد جهات اتصال محظورة</string>
<string name="blockBlockedUser">تم حظر {name}</string>
<string name="blockDescription">هل أنت متيقِّن من حظر <b>{name}؟</b> المستخدمين المحظورين لايمكنهم إرسال طلبات الرسائل، دعوات المجموعات أو الإتصال بك.</string>
@ -241,7 +241,7 @@
<string name="conversationsNew">مراسلة جديدة</string>
<string name="conversationsNone">لا تملك أي محادثات حتى الآن</string>
<string name="conversationsSendWithEnterKey">ارسل مع مفتاح الدخول</string>
<string name="conversationsSendWithEnterKeyDescription">النقر على مفتاح الدخول سوف يرسل الرسالة بدلا من بدء سطر جديد.</string>
<string name="conversationsSendWithEnterKeyDescription">النقر على Enter سوف يرسل الرسالة بدلاَ من بدء سطر جديد.</string>
<string name="conversationsSettingsAllMedia">جميع الوسائط</string>
<string name="conversationsSpellCheck">التدقيق الإملائي</string>
<string name="conversationsSpellCheckDescription">تفعيل التحقق الإملائي عند كتابة الرسائل.</string>
@ -256,7 +256,7 @@
<string name="databaseOptimizing">تحسين قاعدة البيانات</string>
<string name="debugLog">سجل تصحيح الأخطاء</string>
<string name="decline">أرفض</string>
<string name="delete">أحذف</string>
<string name="delete">حذف</string>
<string name="deleteAfterGroupFirstReleaseConfigOutdated">بعض أجهزتك تستخدم إصدارات قديمة. قد تكون المزامنة غير موثوقة حتى يتم تحديثها.</string>
<string name="deleteAfterGroupPR1BlockThisUser">حظر هذا المستخدم</string>
<string name="deleteAfterGroupPR1BlockUser">حظر مستخدم</string>
@ -318,7 +318,7 @@
<string name="disappearingMessagesFollowSettingOff">لن تختفي الرسائل التي ترسلها بعد الآن. هل أنت متأكد أنك تريد إيقاف <b>إيقاف</b> الرسائل المختفية؟</string>
<string name="disappearingMessagesFollowSettingOn">تعيين رسائلك لتختفي <b>{time} </b> بعد أن تكون <b>{disappearing_messages_type} </b>؟</string>
<string name="disappearingMessagesLegacy">{name} يستخدم عميل قديم. قد لا تعمل الرسائل المختفية على النحو المتوقع.</string>
<string name="disappearingMessagesOnlyAdmins">فقط المسؤولين يمكنهم تغيير هذا الإعداد.</string>
<string name="disappearingMessagesOnlyAdmins">يمكن لمشرفين المجموعة فقط تغيير هذا الإعداد.</string>
<string name="disappearingMessagesSent">تم الإرسال</string>
<string name="disappearingMessagesSet"><b>{name}</b> قام بتعيين الرسائل لتختفي بعد {time} من {disappearing_messages_type}.</string>
<string name="disappearingMessagesSetYou"><b>أنت</b> قمت بتعيين الرسائل لتختفي بعد {time} من {disappearing_messages_type}.</string>
@ -346,16 +346,16 @@
<string name="downloading">جارٍ التنزيل...</string>
<string name="draft">مسودة</string>
<string name="edit">تعديل</string>
<string name="emojiAndSymbols">إيموجي &amp;amp; رموز</string>
<string name="emojiAndSymbols">إيموجي و رموز</string>
<string name="emojiCategoryActivities">نشاطات</string>
<string name="emojiCategoryAnimals">حيوانات &amp;amp; و طبيعة</string>
<string name="emojiCategoryFlags">أعلام</string>
<string name="emojiCategoryFood">مأكولات &amp;amp; و مشروبات</string>
<string name="emojiCategoryFood">مأكولات و مشروبات</string>
<string name="emojiCategoryObjects">أجسام</string>
<string name="emojiCategoryRecentlyUsed">مستخدمة حديثًا</string>
<string name="emojiCategorySmileys">ابتسامات &amp;amp; وأشخاص</string>
<string name="emojiCategorySmileys">ابتسامات وأشخاص</string>
<string name="emojiCategorySymbols">رموز</string>
<string name="emojiCategoryTravel">السفر &amp;amp; و أماكن</string>
<string name="emojiCategoryTravel">السفر و أماكن</string>
<string name="emojiReactsClearAll">هل أنت متيقِّن من أنك تريد مسح كافة {emoji}؟</string>
<string name="emojiReactsCoolDown">أبطأ! لقد أرسلت الكثير من ردود الفعل الرموز التعبيرية. حاول مرة أخرى قريبا</string>
<string name="emojiReactsHoverNameDesktop">{name} تفاعل بـ {emoji_name}</string>
@ -364,7 +364,7 @@
<string name="emojiReactsHoverYouNameDesktop">تفاعلت مع {emoji_name}</string>
<string name="emojiReactsHoverYouNameMultipleDesktop">تفاعلت أنت و<span>{count} آخرين</span> مع {emoji_name}</string>
<string name="emojiReactsHoverYouNameTwoDesktop">تفاعلت أنت و{name} مع {emoji_name}</string>
<string name="emojiReactsNotification">تفاعل مع رسالتك {emoji}</string>
<string name="emojiReactsNotification">تفاعل مع رسالتك بـ {emoji}</string>
<string name="enable">تفعيل</string>
<string name="errorConnection">الرجاء التحقق من اتصالك بالإنترنت وحاول مرة أخرى.</string>
<string name="errorCopyAndQuit">نسخ الخطأ والخروج</string>
@ -376,7 +376,7 @@
<string name="followSystemSettings">طابق إعدادات النظام</string>
<string name="from">مِن</string>
<string name="fullScreenToggle">تحويل الشاشة كاملة</string>
<string name="gif">صورة GIF</string>
<string name="gif">GIF</string>
<string name="giphyWarning">Giphy</string>
<string name="giphyWarningDescription">{app_name} سيتصل بمنصة Giphy لتقديم نتائج البحث. لن يكون لديك حماية كاملة للبيانات الوصفية عند إرسال الصور المتحركة (GIFs).</string>
<string name="groupAddMemberMaximum">تضم المجموعات بحد أقصى 100 عضو</string>
@ -397,7 +397,7 @@
<string name="groupInviteFailedTwo">فشل دعوة {name} و {other_name} إلى {group_name}</string>
<string name="groupInviteFailedUser">فشل دعوة {name} إلى {group_name}</string>
<string name="groupInviteSent">تم إرسال الدعوة</string>
<string name="groupInviteSuccessful">الدعوة إلى المجموعة ناجحة</string>
<string name="groupInviteSuccessful">تمت دعوة المجموعة بنجاح</string>
<string name="groupInviteVersion">يجب أن يمتلك المستخدمون الإصدار الأحدث لتلقي الدعوات</string>
<string name="groupInviteYou"><b>أنت</b> تمت دعوتك للانضمام إلى المجموعة.</string>
<string name="groupInviteYouAndMoreNew"><b>أنت</b> و<b>{count} آخرين</b> انضموا للمجموعة.</string>
@ -425,7 +425,7 @@
<string name="groupNameEnter">أدخل اسم المجموعة</string>
<string name="groupNameEnterPlease">الرجاء إدخال اسم للمجموعة.</string>
<string name="groupNameEnterShorter">الرجاء إدخال اسم مجموعة أقصر.</string>
<string name="groupNameNew">أسم المجموعة الآن \'{group_name}.</string>
<string name="groupNameNew">اسم المجموعة الآن \'{group_name}.</string>
<string name="groupNameUpdated">تم تحديث اسم المجموعة.</string>
<string name="groupNoMessages">ليس لديك رسائل من <b>{group_name}</b>. أرسل رسالة لبدء المحادثة!</string>
<string name="groupPromotedYou"><b>أنت</b> تم ترقيتك إلى مشرف.</string>
@ -464,7 +464,7 @@
<string name="helpReportABug">الإبلاغ عن خطأ</string>
<string name="helpReportABugDescription">شارك بعض التفاصيل لمساعدتنا في حل مشكلتك. صدّر السجلات الخاصة بك، ثم قم بتحميل الملف عبر مكتب المساعدة الخاص بـ {app_name}.</string>
<string name="helpReportABugExportLogs">تصدير السجلات</string>
<string name="helpReportABugExportLogsDescription">اصدر السجلات الخاصة بك، ثم ارفع الملف عبر مكتب المساعدة الخاص بـ{app_name}.</string>
<string name="helpReportABugExportLogsDescription">إصدار السجلات الخاصة بك، ثم رفع الملف عبر مكتب المساعدة الخاص بـ{app_name}.</string>
<string name="helpReportABugExportLogsSaveToDesktop">حفظ على سطح المكتب</string>
<string name="helpReportABugExportLogsSaveToDesktopDescription">احفظ هذا الملف على سطح المكتب، ثم شاركه مع مطوري {app_name}.</string>
<string name="helpSupport">الدعم</string>
@ -521,15 +521,15 @@
<item quantity="other">%1$d عضو</item>
</plurals>
<plurals name="membersActive">
<item quantity="zero">%1$d عضو</item>
<item quantity="one">%1$d عضو</item>
<item quantity="two">%1$d عضو</item>
<item quantity="few">%1$d عضو</item>
<item quantity="many">%1$d عضو</item>
<item quantity="zero">%1$d عضو نشط</item>
<item quantity="one">%1$d عضو نشط</item>
<item quantity="two">%1$d عضو نشط</item>
<item quantity="few">%1$d عضو نشط</item>
<item quantity="many">%1$d عضو نشط</item>
<item quantity="other">%1$d عضو نشط</item>
</plurals>
<string name="membersAddAccountIdOrOns">أضف معرف الحساب أو ONS</string>
<string name="membersInvite">دعوة المتصلين</string>
<string name="membersInvite">دعوة جهات الاتصال</string>
<plurals name="membersInviteSend">
<item quantity="zero">إرسال دعوات</item>
<item quantity="one">إرسال دعوة</item>
@ -551,8 +551,8 @@
<string name="messageErrorOld">تلقينا رسالة مشفرة باستخدام إصدار قديم من {app_name} لم يعد مدعومًا. يرجى مطالبة المرسل بتحديث إلى أحدث إصدار وإعادة إرسال الرسالة.</string>
<string name="messageErrorOriginal">لم يتم العثور على الرسالة الأصلية</string>
<string name="messageInfo">معلومات الرسالة</string>
<string name="messageMarkRead">اعتبرها مقروءة</string>
<string name="messageMarkUnread">اجعله/ها غير مقروءة</string>
<string name="messageMarkRead">تحديد كـ \"مقروء\"</string>
<string name="messageMarkUnread">تحديد كـ \"غير مقروء\"</string>
<plurals name="messageNew">
<item quantity="zero">رسائل جديدة</item>
<item quantity="one">رسالة جديدة</item>
@ -573,32 +573,32 @@
</plurals>
<string name="messageReplyingTo">الرد على</string>
<string name="messageRequestGroupInvite"><b>{name}</b> دعاك للانضمام إلى <b>{group_name}</b>.</string>
<string name="messageRequestGroupInviteDescription">إرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة.</string>
<string name="messageRequestGroupInviteDescription">بإرسال رسالة إلى هذه المجموعة سوف يقبل تلقائيًا دعوة المجموعة.</string>
<string name="messageRequestPending">طلب رسالتك قيد الانتظار.</string>
<string name="messageRequestPendingDescription">ستتمكن من إرسال الرسائل الصوتية والمرفقات بمجرد موافقة المستلم على طلب الرسالة هذا.</string>
<string name="messageRequestYouHaveAccepted">لقد وافقتَ على طلب الرسالة من <b>{name}.</b></string>
<string name="messageRequestsAcceptDescription">إرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك.</string>
<string name="messageRequestsAcceptDescription">بإرسال رسالة إلى هذا المستخدم سوف يقبل تلقائيًا طلب الرسالة الخاص به ويكشف عن معرف حسابك.</string>
<string name="messageRequestsAccepted">تم قبول طلب الرسائل الخاص بك.</string>
<string name="messageRequestsClearAllExplanation">هل أنت متأكد من أنك تريد مسح كافة طلبات الرسائل ودعوات المجموعات؟</string>
<string name="messageRequestsCommunities">طلبات رسائل المجتمع</string>
<string name="messageRequestsCommunitiesDescription">السماح بطلبات الرسائل من محادثات المجتمع.</string>
<string name="messageRequestsDelete">هل أنت متأكد من أنك تريد حذف طلب الرسالة هذا؟</string>
<string name="messageRequestsNew">لديك طلب مراسلة جديدة</string>
<string name="messageRequestsNonePending">لا توجد طلبات رسالة معلقة</string>
<string name="messageRequestsNonePending">لا توجد طلبات مراسلة معلقة</string>
<string name="messageRequestsTurnedOff">تم إيقاف طلبات الرسائل من محادثات المجتمع من طرف <b>{name}</b>، لذا لا يمكنك إرسال الرسالة إليه.</string>
<string name="messageSelect">حدد الرسالة</string>
<string name="messageSelect">تحديد رسالة</string>
<string name="messageSnippetGroup">{author}: {message_snippet}</string>
<string name="messageStatusFailedToSend">فشل الإرسال</string>
<string name="messageStatusFailedToSync">فشلت المزامنة</string>
<string name="messageStatusSyncing">جارٍ المزامنة</string>
<string name="messageUnread">الرسائل غير المقروءة</string>
<string name="messageVoice">رسالة صوتية</string>
<string name="messageVoiceErrorShort">اضغط باستمرار لتسجيل رسالة صوتية</string>
<string name="messageVoiceErrorShort">اضغط مع الاستمرار لتسجيل رسالة صوتية</string>
<string name="messageVoiceSlideToCancel">اسحب للإلغاء</string>
<string name="messageVoiceSnippet">{emoji} رسالة صوتية</string>
<string name="messageVoiceSnippetGroup">{author}: {emoji} رسالة صوتية</string>
<string name="messages">الرسائل</string>
<string name="minimize">صغِّر</string>
<string name="minimize">تصغير</string>
<string name="next">التالي</string>
<string name="nicknameDescription">اختر اسم مستعار لـ <b>{name}</b>. سيظهر لك في محادثاتك الفردية والجماعية.</string>
<string name="nicknameEnter">أدخل اسم مستعار</string>
@ -610,21 +610,21 @@
<string name="notNow">ليس الآن</string>
<string name="noteToSelf">ملاحظة لنفسي</string>
<string name="noteToSelfEmpty">ليس لديك أي رسائل في ملاحظة لنفسي أو بمعنى آخر في الرسائل المحفوظة.</string>
<string name="noteToSelfHide">إخفاء ملاحظة لنفسي</string>
<string name="noteToSelfHide">إخفاء \"ملاحظة لنفسي\"</string>
<string name="noteToSelfHideDescription">هل أنت متأكد من أنك تريد إخفاء الملاحظة لنفسي؟</string>
<string name="notificationsAllMessages">جميع الرسائل</string>
<string name="notificationsContent">محتوى الإشعارات</string>
<string name="notificationsContentDescription">المعلومات معروضة في الإشعارات.</string>
<string name="notificationsContentDescription">المعلومات المعروضة في الإشعارات.</string>
<string name="notificationsContentShowNameAndContent">الاسم والمحتوى</string>
<string name="notificationsContentShowNameOnly">الاسم فقط</string>
<string name="notificationsContentShowNoNameOrContent">بدون اسم او محتوى</string>
<string name="notificationsFastMode">الوضع السريع</string>
<string name="notificationsFastModeDescription">ستتم إعلامك بالرسائل الجديدة بشكل موثوق وفوري باستخدام خوادم إشعارات جوجل.</string>
<string name="notificationsFastModeDescriptionIos">سوف يتم إعلامك برسائل جديدة بشكل موثوق وفوري باستخدام خوادم إشعارات Apple.</string>
<string name="notificationsGoToDevice">اذهب إلى إعدادات تنبيهات الجهاز</string>
<string name="notificationsHeaderAllMessages">التنبيهات - الكل</string>
<string name="notificationsHeaderMentionsOnly">التنبيهات - الإشعارات فقط</string>
<string name="notificationsHeaderMute">التنبيهات - مكتومة</string>
<string name="notificationsGoToDevice">اذهب إلى إعدادات إشعارات الجهاز</string>
<string name="notificationsHeaderAllMessages">الإشعارات - الكل</string>
<string name="notificationsHeaderMentionsOnly">الإشعارات- الإشارات فقط</string>
<string name="notificationsHeaderMute">الإشعارات - مكتومة</string>
<string name="notificationsIosGroup">{name} إلى {conversation_name}</string>
<string name="notificationsIosRestart">ربما تلقيت رسائل أثناء إعادة تشغيل {device} الخاص بك.</string>
<string name="notificationsLedColor">لون ضوء التنبيه LED</string>
@ -645,7 +645,7 @@
<string name="notificationsSystem">رسالة جديدة {message_count} في {conversation_count} محادثات</string>
<string name="notificationsVibrate">الاهتزاز</string>
<string name="off">مغلق</string>
<string name="okay">نعم</string>
<string name="okay">حسناً</string>
<string name="on">يعمل</string>
<string name="onboardingAccountCreate">إنشاء حساب</string>
<string name="onboardingAccountCreated">تم إنشاء الحساب</string>
@ -680,8 +680,8 @@
<string name="passwordCurrentIncorrect">كلمة المرور الحالية غير صحيحة.</string>
<string name="passwordDescription">يتطلب كلمة السر لفتح {app_name}.</string>
<string name="passwordEnter">أدخل كلمة السر</string>
<string name="passwordEnterCurrent">يرجى إدخال كلمة السر الحالية</string>
<string name="passwordEnterNew">يرجى إدخال كلمة السر الجديدة</string>
<string name="passwordEnterCurrent">الرجاء إدخال كلمة السر الحالية</string>
<string name="passwordEnterNew">الرجاء إدخال كلمة السر الجديدة</string>
<string name="passwordError">كلمة المرور يجب ان تحتوي فقط على الاحرف, الارقام و الرموز</string>
<string name="passwordErrorLength">كلمة المرور يجب ان تكون بين 6 و 64 عنصر</string>
<string name="passwordErrorMatch">كلمتا المرور لا تتطابقان</string>
@ -715,9 +715,9 @@
<string name="permissionsStorageSaveDenied">{app_name} يحتاج إذن الوصول إلى التخزين لحفظ الصور ومقاطع الفيديو، ولكن تم رفضه نهائيًا. يرجى الانتقال إلى إعدادات التطبيق، واختيار \"الأذونات\"، وتفعيل \"التخزين\".</string>
<string name="permissionsStorageSend">{app_name} يحتاج إذن الوصول إلى التخزين لإرسال الصور ومقاطع الفيديو.</string>
<string name="pin">ًًًُُثَبت</string>
<string name="pinConversation">ثَبِت المحادثة</string>
<string name="pinConversation">تثبيت المحادثة</string>
<string name="pinUnpin">الغ التثبيت</string>
<string name="pinUnpinConversation">ألغِي تثبيت المحادثة</string>
<string name="pinUnpinConversation">إلغاء تثبيت المحادثة</string>
<string name="preview">معاينة</string>
<string name="profile">الملف الشخصي</string>
<string name="profileDisplayPicture">صورة العرض</string>
@ -727,9 +727,9 @@
<string name="profileErrorUpdate">فشل تحديث الملف الشخصي.</string>
<string name="promote">ترقية</string>
<string name="qrCode">رمز QR</string>
<string name="qrNotAccountId">رمز QR هذا لا يحتوي على معرف حساب</string>
<string name="qrNotAccountId">رمز QR هذا لا يحتوي على مُعرف حساب</string>
<string name="qrNotRecoveryPassword">رمز QR هذا لا يحتوي على عبارة استرداد</string>
<string name="qrScan">امسح رمز الاستجابة السريعة</string>
<string name="qrScan">امسح رمز الاستجابة السريعة QR</string>
<string name="qrView">عرض QR</string>
<string name="qrYoursDescription">يمكن للأصدقاء إرسال رسائل إليك عن طريق مسح رمز QR الخاص بك.</string>
<string name="quit">انهاء {app_name}</string>
@ -752,7 +752,7 @@
<string name="recoveryPasswordHidePermanently">إخفاء كلمة مرور الاسترداد بشكل دائم</string>
<string name="recoveryPasswordHidePermanentlyDescription1">بدون كلمة المرور الاستردادية، لا يمكنك تحميل حسابك على الأجهزة الجديدة. \n\nنوصيك بشدة بحفظ كلمة المرور الاستردادية في مكان آمن قبل المتابعة.</string>
<string name="recoveryPasswordHidePermanentlyDescription2">هل أنت متأكد من أنك تريد إخفاء كلمة مرور الاسترداد الخاصة بك على هذا الجهاز نهائيًا؟ لا يمكن التراجع عن هذا.</string>
<string name="recoveryPasswordHideRecoveryPassword">إخفاء كلمة المرور للاسترجاع</string>
<string name="recoveryPasswordHideRecoveryPassword">إخفاء كلمة مرور الاسترداد</string>
<string name="recoveryPasswordHideRecoveryPasswordDescription">إخفاء كلمة المرور الخاصة بالاسترداد على هذا الجهاز بشكل دائم.</string>
<string name="recoveryPasswordRestoreDescription">أدخل كلمة مرور الاسترجاع لتحميل حسابك. إذا لم تقم بحفظها، يمكنك العثور عليها في إعدادات التطبيق.</string>
<string name="recoveryPasswordView">عرض كلمة المرور</string>
@ -778,17 +778,17 @@
<string name="search">بحث</string>
<string name="searchContacts">ابحث في جهات الاتصال</string>
<string name="searchConversation">بحث عن محادثة</string>
<string name="searchEnter">الرجاء إدخال كملة بحث.</string>
<string name="searchEnter">الرجاء إدخال كلمة للبحث.</string>
<plurals name="searchMatches">
<item quantity="zero">%1$d من %2$d مطابقة</item>
<item quantity="one">%1$d من %2$d إجابة</item>
<item quantity="two">%1$d من %2$d مطابقات</item>
<item quantity="one">%1$d من %2$d مطابقة</item>
<item quantity="two">%1$d من %2$d مطابقتين</item>
<item quantity="few">%1$d من %2$d مطابقات</item>
<item quantity="many">%1$d من %2$d مطابقات</item>
<item quantity="other">%1$d من %2$d مطابقات</item>
</plurals>
<string name="searchMatchesNone">لم يتم العثور على أي نتيجة.</string>
<string name="searchMatchesNoneSpecific">لم يتم العثور على أية نتيجة لـ {query}</string>
<string name="searchMatchesNoneSpecific">لم يتم العثور على نتائج لـ {query}</string>
<string name="searchMembers">بحث عن الأعضاء</string>
<string name="searchSearching">جاري البحث...</string>
<string name="select">حدد</string>
@ -800,7 +800,7 @@
<string name="sessionClearData">مسح البيانات</string>
<string name="sessionConversations">المحادثات</string>
<string name="sessionHelp">المساعدة</string>
<string name="sessionInviteAFriend">أُدعُ صديق</string>
<string name="sessionInviteAFriend">دعوة صديق</string>
<string name="sessionMessageRequests">طلبات المُراسلة</string>
<string name="sessionNotifications">الإشعارات</string>
<string name="sessionPermissions">الصلاحيات</string>
@ -818,8 +818,8 @@
<string name="showAll">إظهار الكل</string>
<string name="showLess">عرض أقل</string>
<string name="stickers">الملصقات</string>
<string name="supportGoTo">اِذهب اِلى صفحة الدعم</string>
<string name="systemInformationDesktop">System Information: {information}</string>
<string name="supportGoTo">الذهاب لصفحة الدعم</string>
<string name="systemInformationDesktop">معلومات النظام: {information}</string>
<string name="theContinue">التالي</string>
<string name="theDefault">افتراضي</string>
<string name="theError">خطأ</string>
@ -845,7 +845,7 @@
<string name="urlOpenDescription">هل أنت متأكد من أنك تريد فتح هذا الرابط في متصفحك؟\n\n<b>{url}</b></string>
<string name="useFastMode">استخدم الوضع السريع</string>
<string name="video">فيديو</string>
<string name="videoErrorPlay">غير قادر على تشغيل الفيديو.</string>
<string name="videoErrorPlay">تعذر تشغيل الفيديو</string>
<string name="view">عرض</string>
<string name="waitFewMinutes">قد يستغرق ذلك بضع دقائق.</string>
<string name="waitOneMoment">لحظة واحدة من فضلك...</string>

@ -412,6 +412,7 @@
<string name="groupCreateErrorNoMembers">Please pick at least one other group member.</string>
<string name="groupDelete">Delete Group</string>
<string name="groupDeleteDescription">Are you sure you want to delete <b>{group_name}</b>? This will remove all members and delete all group content.</string>
<string name="groupDeleteDescriptionMember">Are you sure you want to delete <b>{group_name}</b>?</string>
<string name="groupDeletedMemberDescription">{group_name} has been deleted by a group admin. You will not be able to send any more messages.</string>
<string name="groupDescriptionEnter">Enter a group description</string>
<string name="groupDisplayPictureUpdated">Group display picture updated.</string>

Loading…
Cancel
Save