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

Merge Release/1.20.8 back into master
pull/1709/head
SessionHero01 5 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" it.exclude module: "commons-logging"
} }
def canonicalVersionCode = 389 def canonicalVersionCode = 390
def canonicalVersionName = "1.20.7" def canonicalVersionName = "1.20.8"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,
@ -299,7 +299,7 @@ dependencies {
implementation 'androidx.media3:media3-ui:1.4.0' implementation 'androidx.media3:media3-ui:1.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
implementation 'org.signal:aesgcmprovider:0.0.3' 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 "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9' implementation 'com.jpardogo.materialtabstrip:library:1.0.9'

@ -29,38 +29,49 @@
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> 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" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <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.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" /> <!-- For video calls -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <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="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<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_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_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_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<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.RECEIVE_BOOT_COMPLETED" /> <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.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.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Only used on Android API 29 and lower -->
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <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="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" /> <uses-permission android:name="network.loki.messenger.ACCESS_SESSION_SECRETS" />
<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"/>
<queries> <queries>
<intent> <intent>
@ -304,8 +315,10 @@
</activity> </activity>
<activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" /> <activity android:name="org.thoughtcrime.securesms.media.MediaOverviewActivity" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService" <service
android:foregroundServiceType="microphone" android:enabled="true"
android:name="org.thoughtcrime.securesms.service.WebRtcCallService"
android:foregroundServiceType="phoneCall"
android:exported="false" /> android:exported="false" />
<service <service
android:name="org.thoughtcrime.securesms.service.KeyCachingService" 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.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.app.KeyguardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.PowerManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.content.pm.ShortcutManagerCompat;
@ -33,7 +34,20 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner; 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.conscrypt.Conscrypt;
import org.session.libsession.database.MessageDataProvider; import org.session.libsession.database.MessageDataProvider;
import org.session.libsession.messaging.MessagingModuleConfiguration; 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.ConfigFactoryUpdateListener;
import org.session.libsession.utilities.Device; import org.session.libsession.utilities.Device;
import org.session.libsession.utilities.Environment; import org.session.libsession.utilities.Environment;
import org.session.libsession.utilities.NonTranslatableStringConstants;
import org.session.libsession.utilities.ProfilePictureUtilities; import org.session.libsession.utilities.ProfilePictureUtilities;
import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.SSKEnvironment;
import org.session.libsession.utilities.TextSecurePreferences; 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.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher; import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.PeerConnectionFactory.InitializationOptions;
import org.webrtc.PeerConnectionFactory;
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;
/** /**
* Will be called once when the TextSecure process is created. * Will be called once when the TextSecure process is created.
@ -148,7 +146,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; 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 @Override
public Object getSystemService(String name) { 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 // 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") @SuppressLint("ApplySharedPref")
public boolean clearAllData() { public boolean clearAllData() {
TextSecurePreferences.clearAll(this); TextSecurePreferences.clearAll(this);
@ -492,4 +487,35 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
} }
// endregion // 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.SpannableString;
import android.text.style.RelativeSizeSpan; import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.List;
import java.util.Objects;
import org.session.libsession.messaging.calls.CallMessageType; import org.session.libsession.messaging.calls.CallMessageType;
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage; import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage;
import org.session.libsession.messaging.utilities.UpdateMessageBuilder; 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.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; 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 * The base class for message record models that are displayed in
* conversations, as opposed to models that are displayed in a thread list. * conversations, as opposed to models that are displayed in a thread list.

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

@ -3,20 +3,18 @@ package org.thoughtcrime.securesms.notifications;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; 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 { public class NotificationState {
@ -36,68 +34,47 @@ public class NotificationState {
} }
public void addNotification(NotificationItem item) { public void addNotification(NotificationItem item) {
// Add this new notification at the beginning of the list
notifications.addFirst(item); notifications.addFirst(item);
if (threads.contains(item.getThreadId())) { // Put a notification at the front by removing it then re-adding it?
threads.remove(item.getThreadId()); threads.remove(item.getThreadId());
}
threads.add(item.getThreadId()); threads.add(item.getThreadId());
notificationCount++; notificationCount++;
} }
public @Nullable Uri getRingtone(@NonNull Context context) { public @Nullable Uri getRingtone(@NonNull Context context) {
if (!notifications.isEmpty()) { if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient(); 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() { public VibrateState getVibrate() {
if (!notifications.isEmpty()) { if (!notifications.isEmpty()) {
Recipient recipient = notifications.getFirst().getRecipient(); Recipient recipient = notifications.getFirst().getRecipient();
if (recipient != null) {
return recipient.resolve().getMessageVibrate(); return recipient.resolve().getMessageVibrate();
} }
}
return VibrateState.DEFAULT; return VibrateState.DEFAULT;
} }
public boolean hasMultipleThreads() { public boolean hasMultipleThreads() { return threads.size() > 1; }
return threads.size() > 1; public LinkedHashSet<Long> getThreads() { return threads; }
} public int getThreadCount() { return threads.size(); }
public int getNotificationCount() { return notificationCount; }
public LinkedHashSet<Long> getThreads() { public List<NotificationItem> getNotifications() { return notifications; }
return threads;
}
public int getThreadCount() {
return threads.size();
}
public int getMessageCount() {
return notificationCount;
}
public List<NotificationItem> getNotifications() {
return notifications;
}
public List<NotificationItem> getNotificationsForThread(long threadId) { public List<NotificationItem> getNotificationsForThread(long threadId) {
LinkedList<NotificationItem> list = new LinkedList<>(); LinkedList<NotificationItem> notificationsInThread = new LinkedList<>();
for (NotificationItem item : notifications) { 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) { public PendingIntent getMarkAsReadIntent(Context context, int notificationId) {
@ -223,6 +200,4 @@ public class NotificationState {
return PendingIntent.getBroadcast(context, 0, intent, intentFlags); 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)) .put(APP_NAME_KEY, c.getString(R.string.app_name))
.format().toString(); .format().toString();
builder.setContentTitle(unlockedTxt); builder.setContentTitle(unlockedTxt);
builder.setContentText(getString(R.string.lockAppUnlock));
builder.setSmallIcon(R.drawable.icon_cached); builder.setSmallIcon(R.drawable.icon_cached);
builder.setWhen(0); builder.setWhen(0);
builder.setPriority(Notification.PRIORITY_MIN); builder.setPriority(Notification.PRIORITY_MIN);

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent 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.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -19,11 +19,18 @@ import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint 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.messaging.calls.CallMessageType
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.FutureTaskListener import org.session.libsession.utilities.FutureTaskListener
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.calls.WebRtcCallActivity import org.thoughtcrime.securesms.calls.WebRtcCallActivity
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker import org.thoughtcrime.securesms.notifications.BackgroundPollWorker
import org.thoughtcrime.securesms.util.CallNotificationBuilder 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_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING 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.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallManager import org.thoughtcrime.securesms.webrtc.CallManager
import org.thoughtcrime.securesms.webrtc.CallViewModel 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.WiredHeadsetStateReceiver
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event 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.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.DataChannel import org.webrtc.DataChannel
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
@ -54,13 +63,6 @@ import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
import org.webrtc.PeerConnection.IceConnectionState.FAILED import org.webrtc.PeerConnection.IceConnectionState.FAILED
import org.webrtc.RtpReceiver import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription 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 @AndroidEntryPoint
class WebRtcCallService : LifecycleService(), CallManager.WebRtcListener { 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) { private fun handleRemoteIceCandidate(intent: Intent) {
val callId = getCallId(intent) val callId = getCallId(intent)
val sdpMids = intent.getStringArrayExtra(EXTRA_ICE_SDP_MID) ?: return 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?) { private fun setCallInProgressNotification(type: Int, recipient: Recipient?) {
// 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 { try {
ServiceCompat.startForeground( ServiceCompat.startForeground(
this, this,
CallNotificationBuilder.WEBRTC_NOTIFICATION, WEBRTC_NOTIFICATION,
CallNotificationBuilder.getCallInProgressNotification(this, type, recipient), CallNotificationBuilder.getCallInProgressNotification(this, type, recipient),
if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE else 0 if (Build.VERSION.SDK_INT >= 30) ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL else 0
) )
return
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
Log.e(TAG, "Failed to setCallInProgressNotification as a foreground service for type: ${type}, trying to update instead", e) 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 // Start an intent for the fullscreen call activity
val foregroundIntent = Intent(this, WebRtcCallActivity::class.java) 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) .setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
startActivity(foregroundIntent) startActivity(foregroundIntent)
return
} }
} }

@ -37,31 +37,6 @@ class CallNotificationBuilder {
return notificationManager.areNotificationsEnabled() 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 @JvmStatic
fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification { fun getCallInProgressNotification(context: Context, type: Int, recipient: Recipient?): Notification {
val contentIntent = Intent(context, WebRtcCallActivity::class.java) val contentIntent = Intent(context, WebRtcCallActivity::class.java)
@ -98,9 +73,7 @@ class CallNotificationBuilder {
R.string.decline R.string.decline
)) ))
// If notifications aren't enabled, we will trigger the intent from WebRtcCallService // If notifications aren't enabled, we will trigger the intent from WebRtcCallService
builder.setFullScreenIntent(getFullScreenPendingIntent( builder.setFullScreenIntent(getFullScreenPendingIntent(context), true)
context
), true)
builder.addAction(getActivityNotificationAction( builder.addAction(getActivityNotificationAction(
context, context,
if (type == TYPE_INCOMING_PRE_OFFER) WebRtcCallActivity.ACTION_PRE_OFFER else WebRtcCallActivity.ACTION_ANSWER, 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 { private fun getFullScreenPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, WebRtcCallActivity::class.java) 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) .setAction(WebRtcCallActivity.ACTION_FULL_SCREEN_INTENT)
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE) return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} }

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.webrtc package org.thoughtcrime.securesms.webrtc
import android.Manifest import android.Manifest
import android.app.NotificationManager import android.app.KeyguardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.PowerManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope 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.messaging.utilities.WebRtcUtils
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.NonTranslatableStringConstants
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ANSWER 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.PRE_OFFER
import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.PROVISIONAL_ANSWER
import org.session.libsignal.utilities.Log 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.permissions.Permissions
import org.thoughtcrime.securesms.service.WebRtcCallService import org.thoughtcrime.securesms.service.WebRtcCallService
import org.thoughtcrime.securesms.util.CallNotificationBuilder
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) { class CallMessageProcessor(private val context: Context, private val textSecurePreferences: TextSecurePreferences, lifecycle: Lifecycle, private val storage: StorageProtocol) {
companion object { companion object {
private const val TAG = "CallMessageProcessor"
private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L private const val VERY_EXPIRED_TIME = 15 * 60 * 1000L
fun safeStartService(context: Context, intent: Intent) { fun safeStartForegroundService(context: Context, intent: Intent) {
// If the foreground service crashes then it's possible for one of these intents to // Wake up the device (if required) before attempting to start any services - otherwise on Android 12 and above we get
// be started in the background (in which case 'startService' will throw a // a BackgroundServiceStartNotAllowedException such as:
// 'BackgroundServiceStartNotAllowedException' exception) so catch that case and try // Unable to start CallMessage intent: startForegroundService() not allowed due to mAllowStartForeground false:
// to re-start the service in the foreground // service network.loki.messenger/org.thoughtcrime.securesms.service.WebRtcCallService
try { context.startService(intent) } (context as ApplicationContext).wakeUpDeviceAndDismissKeyguardIfRequired()
catch(e: Exception) {
try { ContextCompat.startForegroundService(context, intent) } // Attempt to start the call service..
catch (e2: Exception) { try {
Log.e("Loki", "Unable to start CallMessage intent: ${e2.message}") 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") Log.i("Loki", "Contact is approved?: $approvedContact")
if (!approvedContact && storage.getUserPublicKey() != sender) continue if (!approvedContact && storage.getUserPublicKey() != sender) continue
// if the user has not enabled voice/video calls // If the user has not enabled voice/video calls or if the user has not granted audio/microphone permissions
// or if the user has not granted audio/microphone permissions
if ( if (
!textSecurePreferences.isCallNotificationsEnabled() || !textSecurePreferences.isCallNotificationsEnabled() ||
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) !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) { private fun incomingHangup(callMessage: CallMessage) {
val callId = callMessage.callId ?: return val callId = callMessage.callId ?: return
val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId) val hangupIntent = WebRtcCallService.remoteHangupIntent(context, callId)
safeStartService(context, hangupIntent) safeStartForegroundService(context, hangupIntent)
} }
private fun incomingAnswer(callMessage: CallMessage) { private fun incomingAnswer(callMessage: CallMessage) {
val recipientAddress = callMessage.sender ?: return val recipientAddress = callMessage.sender ?: return Log.w(TAG, "Cannot answer incoming call without sender")
val callId = callMessage.callId ?: return val callId = callMessage.callId ?: return Log.w(TAG, "Cannot answer incoming call without callId" )
val sdp = callMessage.sdps.firstOrNull() ?: return val sdp = callMessage.sdps.firstOrNull() ?: return Log.w(TAG, "Cannot answer incoming call without sdp")
val answerIntent = WebRtcCallService.incomingAnswer( val answerIntent = WebRtcCallService.incomingAnswer(
context = context, context = context,
address = Address.fromSerialized(recipientAddress), address = Address.fromSerialized(recipientAddress),
sdp = sdp, sdp = sdp,
callId = callId callId = callId
) )
safeStartForegroundService(context, answerIntent)
safeStartService(context, answerIntent)
} }
private fun handleIceCandidates(callMessage: CallMessage) { private fun handleIceCandidates(callMessage: CallMessage) {
@ -131,7 +138,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
address = Address.fromSerialized(sender) address = Address.fromSerialized(sender)
) )
safeStartService(context, iceIntent) safeStartForegroundService(context, iceIntent)
} }
private fun incomingPreOffer(callMessage: CallMessage) { private fun incomingPreOffer(callMessage: CallMessage) {
@ -144,7 +151,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
safeStartService(context, incomingIntent) safeStartForegroundService(context, incomingIntent)
} }
private fun incomingCall(callMessage: CallMessage) { private fun incomingCall(callMessage: CallMessage) {
@ -158,7 +165,7 @@ class CallMessageProcessor(private val context: Context, private val textSecureP
callId = callId, callId = callId,
callTime = callMessage.sentTimestamp!! callTime = callMessage.sentTimestamp!!
) )
safeStartService(context, incomingIntent) safeStartForegroundService(context, incomingIntent)
} }
private fun CallMessage.iceCandidates(): List<IceCandidate> { 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.ConversationVolatileConfig
import network.loki.messenger.libsession_util.UserGroupsConfig import network.loki.messenger.libsession_util.UserGroupsConfig
import network.loki.messenger.libsession_util.UserProfile 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.Deferred
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred 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.Namespace
import org.session.libsignal.utilities.Snode import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.Util.SECURE_RANDOM 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" private const val TAG = "Poller"
@ -44,8 +45,9 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
// region Settings // region Settings
companion object { companion object {
private const val retryInterval: Long = 2 * 1000 private const val RETRY_INTERVAL_MS: Long = 2 * 1000
private const val maxInterval: Long = 15 * 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 // endregion
@ -54,7 +56,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
if (hasStarted) { return } if (hasStarted) { return }
Log.d(TAG, "Started polling.") Log.d(TAG, "Started polling.")
hasStarted = true hasStarted = true
setUpPolling(retryInterval) setUpPolling(RETRY_INTERVAL_MS)
} }
fun stopIfNeeded() { fun stopIfNeeded() {
@ -67,9 +69,11 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
Log.d(TAG, "Retrieving user profile.") Log.d(TAG, "Retrieving user profile.")
SnodeAPI.getSwarm(userPublicKey).bind { SnodeAPI.getSwarm(userPublicKey).bind {
usedSnodes.clear() usedSnodes.clear()
deferred<Unit, Exception>().also { deferred<Unit, Exception>().also { exception ->
pollNextSnode(userProfileOnly = true, it) pollNextSnode(userProfileOnly = true, exception)
}.promise }.promise
}.fail { exception ->
Log.e(TAG, "Failed to retrieve user profile.", exception)
} }
} }
// endregion // endregion
@ -84,14 +88,14 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
pollNextSnode(deferred = deferred) pollNextSnode(deferred = deferred)
deferred.promise deferred.promise
}.success { }.success {
val nextDelay = if (isCaughtUp) retryInterval else 0 val nextDelay = if (isCaughtUp) RETRY_INTERVAL_MS else 0
Timer().schedule(object : TimerTask() { Timer().schedule(object : TimerTask() {
override fun run() { override fun run() {
thread.run { setUpPolling(retryInterval) } thread.run { setUpPolling(RETRY_INTERVAL_MS) }
} }
}, nextDelay) }, nextDelay)
}.fail { }.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() { Timer().schedule(object : TimerTask() {
override fun run() { override fun run() {
thread.run { setUpPolling(nextDelay) } thread.run { setUpPolling(nextDelay) }

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

@ -412,6 +412,7 @@
<string name="groupCreateErrorNoMembers">Please pick at least one other group member.</string> <string name="groupCreateErrorNoMembers">Please pick at least one other group member.</string>
<string name="groupDelete">Delete Group</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="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="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="groupDescriptionEnter">Enter a group description</string>
<string name="groupDisplayPictureUpdated">Group display picture updated.</string> <string name="groupDisplayPictureUpdated">Group display picture updated.</string>

Loading…
Cancel
Save