From 025673513564462e9c5bc1b6cc11d57c9d23a9d9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 3 Feb 2023 13:33:52 +1100 Subject: [PATCH 1/3] Fixed a few bugs, added logging and removed some old code Added the ability to copy the sessionId of open group URL from the conversation menu Added additional logging to the BatchMessageReceiveJob to make future debugging easier Removed the OpenGroupMigrator Updated the JobQueue logging to provide more insight Fixed an issue where the database migrations weren't blocking which could result in failing/crashing SQL queries Fixed an issue where the new database file wouldn't be removed if a migration error was thrown Fixed an issue where the new database could exist in an invalid state and the app wouldn't attempt to remigrate Fixed an incorrectly throw exception in the PassphrasePromptActivity --- .../securesms/ApplicationContext.java | 4 - .../securesms/PassphrasePromptActivity.java | 3 +- .../conversation/v2/ConversationActivityV2.kt | 12 + .../v2/menus/ConversationMenuHelper.kt | 12 + .../securesms/database/ThreadDatabase.java | 72 ----- .../database/helpers/SQLCipherOpenHelper.java | 95 ++++-- .../securesms/groups/OpenGroupMigrator.kt | 139 --------- .../notifications/BackgroundPollWorker.kt | 2 +- .../res/menu/menu_conversation_open_group.xml | 4 + app/src/main/res/values/strings.xml | 1 + .../securesms/util/OpenGroupMigrationTests.kt | 281 ------------------ .../messaging/jobs/AttachmentDownloadJob.kt | 22 +- .../messaging/jobs/AttachmentUploadJob.kt | 24 +- .../messaging/jobs/BackgroundGroupAddJob.kt | 8 +- .../messaging/jobs/BatchMessageReceiveJob.kt | 20 +- .../messaging/jobs/GroupAvatarDownloadJob.kt | 6 +- .../session/libsession/messaging/jobs/Job.kt | 2 +- .../libsession/messaging/jobs/JobDelegate.kt | 6 +- .../libsession/messaging/jobs/JobQueue.kt | 34 ++- .../messaging/jobs/MessageReceiveJob.kt | 24 +- .../messaging/jobs/MessageSendJob.kt | 28 +- .../messaging/jobs/NotifyPNServerJob.kt | 14 +- .../messaging/jobs/OpenGroupDeleteJob.kt | 6 +- .../messaging/jobs/TrimThreadJob.kt | 4 +- 24 files changed, 204 insertions(+), 619 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt delete mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index ef4f5c46a3..a95b6c28c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseModule; import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.groups.OpenGroupManager; -import org.thoughtcrime.securesms.groups.OpenGroupMigrator; import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; @@ -206,9 +205,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO storage, messageDataProvider, ()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this)); - // migrate session open group data - OpenGroupMigrator.migrate(getDatabaseComponent()); - // end migration callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage); Log.i(TAG, "onCreate()"); startKovenant(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java index 63b42c4936..afc993df8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphrasePromptActivity.java @@ -210,8 +210,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity { try { signature = biometricSecretProvider.getOrCreateBiometricSignature(this); hasSignatureObject = true; - throw new InvalidKeyException("e"); - } catch (InvalidKeyException e) { + } catch (Exception e) { signature = null; hasSignatureObject = false; Log.e(TAG, "Error getting / creating signature", e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7adf5f7dd3..8ba8f10688 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -963,6 +963,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } + override fun copyOpenGroupUrl(thread: Recipient) { + if (!thread.isOpenGroupRecipient) { return } + + val threadId = threadDb.getThreadIdIfExistsFor(thread) ?: return + val openGroup = lokiThreadDb.getOpenGroupChat(threadId) ?: return + + val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) + val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + override fun showExpiringMessagesDialog(thread: Recipient) { if (thread.isClosedGroupRecipient) { val group = groupDb.getGroup(thread.address.toGroupString()).orNull() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 663dd2e255..8a6c84aecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -78,6 +78,10 @@ object ConversationMenuHelper { inflater.inflate(R.menu.menu_conversation_expiration_off, menu) } } + // One-on-one chat menu allows copying the session id + if (thread.isContactRecipient) { + inflater.inflate(R.menu.menu_conversation_copy_session_id, menu) + } // One-on-one chat menu (options that should only be present for one-on-one chats) if (thread.isContactRecipient) { if (thread.isBlocked) { @@ -154,6 +158,7 @@ object ConversationMenuHelper { R.id.menu_block -> { block(context, thread, deleteThread = false) } R.id.menu_block_delete -> { blockAndDelete(context, thread) } R.id.menu_copy_session_id -> { copySessionID(context, thread) } + R.id.menu_copy_open_group_url -> { copyOpenGroupUrl(context, thread) } R.id.menu_edit_group -> { editClosedGroup(context, thread) } R.id.menu_leave_group -> { leaveClosedGroup(context, thread) } R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) } @@ -270,6 +275,12 @@ object ConversationMenuHelper { listener.copySessionID(thread.address.toString()) } + private fun copyOpenGroupUrl(context: Context, thread: Recipient) { + if (!thread.isOpenGroupRecipient) { return } + val listener = context as? ConversationMenuListener ?: return + listener.copyOpenGroupUrl(thread) + } + private fun editClosedGroup(context: Context, thread: Recipient) { if (!thread.isClosedGroupRecipient) { return } val intent = Intent(context, EditClosedGroupActivity::class.java) @@ -344,6 +355,7 @@ object ConversationMenuHelper { fun block(deleteThread: Boolean = false) fun unblock() fun copySessionID(sessionId: String) + fun copyOpenGroupUrl(thread: Recipient) fun showExpiringMessagesDialog(thread: Recipient) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 26b20ab00a..976a21595d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; -import org.thoughtcrime.securesms.groups.OpenGroupMigrator; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; @@ -800,77 +799,6 @@ public class ThreadDatabase extends Database { return query; } - @NotNull - public List getHttpOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.HTTP_PREFIX+OpenGroupMigrator.OPEN_GET_SESSION_TRAILING_DOT_ENCODED +"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - - @NotNull - public List getLegacyOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.LEGACY_GROUP_ENCODED_ID+"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - - @NotNull - public List getHttpsOxenOpenGroups() { - String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; - String selection = OpenGroupMigrator.NEW_GROUP_ENCODED_ID+"%"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = createQuery(where, 0); - Cursor cursor = db.rawQuery(query, new String[]{selection}); - if (cursor == null) { - return Collections.emptyList(); - } - List threads = new ArrayList<>(); - try { - Reader reader = readerFor(cursor); - ThreadRecord record; - while ((record = reader.getNext()) != null) { - threads.add(record); - } - } finally { - cursor.close(); - } - return threads; - } - public void migrateEncodedGroup(long threadId, @NotNull String newEncodedGroupId) { ContentValues contentValues = new ContentValues(1); contentValues.put(ADDRESS, newEncodedGroupId); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 4a48aa2446..df69c239fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -97,25 +97,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private final DatabaseSecret databaseSecret; public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { - super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() { - @Override - public void preKey(SQLiteConnection connection) { - SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); - } - - @Override - public void postKey(SQLiteConnection connection) { - SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); - - // if not vacuumed in a while, perform that operation - long currentTime = System.currentTimeMillis(); - // 7 days - if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) { - connection.execute("VACUUM;", null, null); - TextSecurePreferences.setLastVacuumNow(context); + super( + context, + DATABASE_NAME, + databaseSecret.asString(), + null, + DATABASE_VERSION, + MIN_DATABASE_VERSION, + null, + new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); } - } - }, true); + + @Override + public void postKey(SQLiteConnection connection) { + SQLCipherOpenHelper.applySQLCipherPragmas(connection, true); + + // if not vacuumed in a while, perform that operation + long currentTime = System.currentTimeMillis(); + // 7 days + if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) { + connection.execute("VACUUM;", null, null); + TextSecurePreferences.setLastVacuumNow(context); + } + } + }, + // Note: Now that we support concurrent database reads the migrations are actually non-blocking + // because of this we need to initially open the database with writeAheadLogging (WAL mode) disabled + // and enable it once the database officially opens it's connection (which will cause it to re-connect + // in WAL mode) - this is a little inefficient but will prevent SQL-related errors/crashes due to + // incomplete migrations + false + ); this.context = context.getApplicationContext(); this.databaseSecret = databaseSecret; @@ -150,11 +165,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { // If the old SQLCipher3 database file doesn't exist then no need to do anything if (!oldDbFile.exists()) { return; } - try { - // Define the location for the new database - String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); - File newDbFile = new File(newDbPath); + // Define the location for the new database + String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); + File newDbFile = new File(newDbPath); + try { // If the new database file already exists then check if it's valid first, if it's in an // invalid state we should delete it and try to migrate again if (newDbFile.exists()) { @@ -162,10 +177,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { // assume the user hasn't downgraded for some reason and made changes to the old database and // can remove the old database file (it won't be used anymore) if (oldDbFile.lastModified() <= newDbFile.lastModified()) { - // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past -// //noinspection ResultOfMethodCallIgnored -// oldDbFile.delete(); - return; + try { + SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true); + int version = newDb.getVersion(); + newDb.close(); + + // Make sure the new database has it's version set correctly (if not then the migration didn't + // fully succeed and the database will try to create all it's tables and immediately fail so + // we will need to remove and remigrate) + if (version > 0) { + // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past +// //noinspection ResultOfMethodCallIgnored +// oldDbFile.delete(); + return; + } + } + catch (Exception e) { + Log.i(TAG, "Failed to retrieve version from new database, assuming invalid and remigrating"); + } } // If the old database does have newer changes then the new database could have stale/invalid @@ -207,6 +236,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { catch (Exception e) { Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e); + // If an exception was thrown then we should remove the new database file (it's probably invalid) + if (!newDbFile.delete()) { + Log.e(TAG, "Unable to delete invalid new database file"); + } + // Notify the user of the issue so they know they can downgrade until the issue is fixed NotificationManager notificationManager = context.getSystemService(NotificationManager.class); String channelId = context.getString(R.string.NotificationChannel_failures); @@ -559,6 +593,15 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } } + @Override + public void onOpen(SQLiteDatabase db) { + super.onOpen(db); + + // Now that the database is officially open (ie. the migrations are completed) we want to enable + // write ahead logging (WAL mode) to officially support concurrent read connections + db.enableWriteAheadLogging(); + } + public void markCurrent(SQLiteDatabase db) { db.setVersion(DATABASE_VERSION); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt deleted file mode 100644 index 642d191614..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt +++ /dev/null @@ -1,139 +0,0 @@ -package org.thoughtcrime.securesms.groups - -import androidx.annotation.VisibleForTesting -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.utilities.recipients.Recipient -import org.session.libsignal.utilities.Hex -import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent - -object OpenGroupMigrator { - const val HTTP_PREFIX = "__loki_public_chat_group__!687474703a2f2f" - private const val HTTPS_PREFIX = "__loki_public_chat_group__!68747470733a2f2f" - const val OPEN_GET_SESSION_TRAILING_DOT_ENCODED = "6f70656e2e67657473657373696f6e2e6f72672e" - const val LEGACY_GROUP_ENCODED_ID = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e" // old IP based toByteArray() - const val NEW_GROUP_ENCODED_ID = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e" // new URL based toByteArray() - - data class OpenGroupMapping(val stub: String, val legacyThreadId: Long, val newThreadId: Long?) - - @VisibleForTesting - fun Recipient.roomStub(): String? { - if (!isOpenGroupRecipient) return null - val serialized = address.serialize() - if (serialized.startsWith(LEGACY_GROUP_ENCODED_ID)) { - return serialized.replace(LEGACY_GROUP_ENCODED_ID,"") - } else if (serialized.startsWith(NEW_GROUP_ENCODED_ID)) { - return serialized.replace(NEW_GROUP_ENCODED_ID,"") - } else if (serialized.startsWith(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED)) { - return serialized.replace(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED, "") - } - return null - } - - @VisibleForTesting - fun getExistingMappings(legacy: List, new: List): List { - val legacyStubsMapping = legacy.mapNotNull { thread -> - val stub = thread.recipient.roomStub() - stub?.let { it to thread.threadId } - } - val newStubsMapping = new.mapNotNull { thread -> - val stub = thread.recipient.roomStub() - stub?.let { it to thread.threadId } - } - return legacyStubsMapping.map { (legacyEncodedStub, legacyId) -> - // get 'new' open group thread ID if stubs match - OpenGroupMapping( - legacyEncodedStub, - legacyId, - newStubsMapping.firstOrNull { (newEncodedStub, _) -> newEncodedStub == legacyEncodedStub }?.second - ) - } - } - - @JvmStatic - fun migrate(databaseComponent: DatabaseComponent) { - // migrate thread db - val threadDb = databaseComponent.threadDatabase() - - val legacyOpenGroups = threadDb.legacyOxenOpenGroups - val httpBasedNewGroups = threadDb.httpOxenOpenGroups - if (legacyOpenGroups.isEmpty() && httpBasedNewGroups.isEmpty()) return // no need to migrate - - val newOpenGroups = threadDb.httpsOxenOpenGroups - val firstStepMigration = getExistingMappings(legacyOpenGroups, newOpenGroups) - - val secondStepMigration = getExistingMappings(httpBasedNewGroups, newOpenGroups) - - val groupDb = databaseComponent.groupDatabase() - val lokiApiDb = databaseComponent.lokiAPIDatabase() - val smsDb = databaseComponent.smsDatabase() - val mmsDb = databaseComponent.mmsDatabase() - val lokiMessageDatabase = databaseComponent.lokiMessageDatabase() - val lokiThreadDatabase = databaseComponent.lokiThreadDatabase() - - firstStepMigration.forEach { (stub, old, new) -> - val legacyEncodedGroupId = LEGACY_GROUP_ENCODED_ID+stub - if (new == null) { - val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub - // migrate thread and group encoded values - threadDb.migrateEncodedGroup(old, newEncodedGroupId) - groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) - // migrate Loki API DB values - // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" - val decodedStub = Hex.fromStringCondensed(stub).decodeToString() - val legacyLokiServerId = "${OpenGroupApi.legacyDefaultServer}.$decodedStub" - val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" - lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) - // migrate loki thread db server info - val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) - val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) - lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) - } else { - // has a legacy and a new one - // migrate SMS and MMS tables - smsDb.migrateThreadId(old, new) - mmsDb.migrateThreadId(old, new) - lokiMessageDatabase.migrateThreadId(old, new) - // delete group for legacy ID - groupDb.delete(legacyEncodedGroupId) - // delete thread for legacy ID - threadDb.deleteConversation(old) - lokiThreadDatabase.removeOpenGroupChat(old) - } - // maybe migrate jobs here - } - - secondStepMigration.forEach { (stub, old, new) -> - val legacyEncodedGroupId = HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED + stub - if (new == null) { - val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub - // migrate thread and group encoded values - threadDb.migrateEncodedGroup(old, newEncodedGroupId) - groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) - // migrate Loki API DB values - // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" - val decodedStub = Hex.fromStringCondensed(stub).decodeToString() - val legacyLokiServerId = "${OpenGroupApi.httpDefaultServer}.$decodedStub" - val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" - lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) - // migrate loki thread db server info - val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) - val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) - lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) - } else { - // has a legacy and a new one - // migrate SMS and MMS tables - smsDb.migrateThreadId(old, new) - mmsDb.migrateThreadId(old, new) - lokiMessageDatabase.migrateThreadId(old, new) - // delete group for legacy ID - groupDb.delete(legacyEncodedGroupId) - // delete thread for legacy ID - threadDb.deleteConversation(old) - lokiThreadDatabase.removeOpenGroupChat(old) - } - // maybe migrate jobs here - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 5a0438e15d..9b8ea5824d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -60,7 +60,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor // FIXME: Using a job here seems like a bad idea... MessageReceiveParameters(envelope.toByteArray(), serverHash, null) } - BatchMessageReceiveJob(params).executeAsync() + BatchMessageReceiveJob(params).executeAsync("background") } promises.add(dmsPromise) diff --git a/app/src/main/res/menu/menu_conversation_open_group.xml b/app/src/main/res/menu/menu_conversation_open_group.xml index 6ff025aadb..1bbb2d76de 100644 --- a/app/src/main/res/menu/menu_conversation_open_group.xml +++ b/app/src/main/res/menu/menu_conversation_open_group.xml @@ -2,6 +2,10 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d0ee94cf1d..7c39714c09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,6 +77,7 @@ Attachment exceeds size limits for the type of message you\'re sending. Unable to record audio! There is no app available to handle this link on your device. + Copy Community URL Add members Session needs microphone access to send audio messages. Session needs microphone access to send audio messages, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Microphone\". diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt b/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt deleted file mode 100644 index dcf8ca231b..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt +++ /dev/null @@ -1,281 +0,0 @@ -package org.thoughtcrime.securesms.util - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import org.mockito.kotlin.KStubbing -import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoMoreInteractions -import org.session.libsession.messaging.open_groups.OpenGroup -import org.session.libsession.messaging.open_groups.OpenGroupApi -import org.session.libsession.utilities.Address -import org.session.libsession.utilities.recipients.Recipient -import org.thoughtcrime.securesms.database.GroupDatabase -import org.thoughtcrime.securesms.database.LokiAPIDatabase -import org.thoughtcrime.securesms.database.LokiMessageDatabase -import org.thoughtcrime.securesms.database.LokiThreadDatabase -import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.SmsDatabase -import org.thoughtcrime.securesms.database.ThreadDatabase -import org.thoughtcrime.securesms.database.model.ThreadRecord -import org.thoughtcrime.securesms.dependencies.DatabaseComponent -import org.thoughtcrime.securesms.groups.OpenGroupMigrator -import org.thoughtcrime.securesms.groups.OpenGroupMigrator.OpenGroupMapping -import org.thoughtcrime.securesms.groups.OpenGroupMigrator.roomStub - -class OpenGroupMigrationTests { - - companion object { - const val EXAMPLE_LEGACY_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e6f78656e" - const val EXAMPLE_NEW_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e6f78656e" - const val OXEN_STUB_HEX = "6f78656e" - - const val EXAMPLE_LEGACY_SERVER_ID = "http://116.203.70.33.oxen" - const val EXAMPLE_NEW_SERVER_ID = "https://open.getsession.org.oxen" - - const val LEGACY_THREAD_ID = 1L - const val NEW_THREAD_ID = 2L - } - - private fun legacyOpenGroupRecipient(additionalMocks: ((KStubbing) -> Unit) ? = null) = mock { - on { address } doReturn Address.fromSerialized(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP) - on { isOpenGroupRecipient } doReturn true - additionalMocks?.let { it(this) } - } - - private fun newOpenGroupRecipient(additionalMocks: ((KStubbing) -> Unit) ? = null) = mock { - on { address } doReturn Address.fromSerialized(EXAMPLE_NEW_ENCODED_OPEN_GROUP) - on { isOpenGroupRecipient } doReturn true - additionalMocks?.let { it(this) } - } - - private fun legacyThreadRecord(additionalRecipientMocks: ((KStubbing) -> Unit) ? = null, additionalThreadMocks: ((KStubbing) -> Unit)? = null) = mock { - val returnedRecipient = legacyOpenGroupRecipient(additionalRecipientMocks) - on { recipient } doReturn returnedRecipient - on { threadId } doReturn LEGACY_THREAD_ID - } - - private fun newThreadRecord(additionalRecipientMocks: ((KStubbing) -> Unit)? = null, additionalThreadMocks: ((KStubbing) -> Unit)? = null) = mock { - val returnedRecipient = newOpenGroupRecipient(additionalRecipientMocks) - on { recipient } doReturn returnedRecipient - on { threadId } doReturn NEW_THREAD_ID - } - - @Test - fun `it should generate the correct room stubs for legacy groups`() { - val mockRecipient = legacyOpenGroupRecipient() - assertEquals(OXEN_STUB_HEX, mockRecipient.roomStub()) - } - - @Test - fun `it should generate the correct room stubs for new groups`() { - val mockNewRecipient = newOpenGroupRecipient() - assertEquals(OXEN_STUB_HEX, mockNewRecipient.roomStub()) - } - - @Test - fun `it should return correct mappings`() { - val legacyThread = legacyThreadRecord() - val newThread = newThreadRecord() - - val expectedMapping = listOf( - OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, NEW_THREAD_ID) - ) - - assertTrue(expectedMapping.containsAll(OpenGroupMigrator.getExistingMappings(listOf(legacyThread), listOf(newThread)))) - } - - @Test - fun `it should return no mappings if there are no legacy open groups`() { - val mappings = OpenGroupMigrator.getExistingMappings(listOf(), listOf()) - assertTrue(mappings.isEmpty()) - } - - @Test - fun `it should return no mappings if there are only new open groups`() { - val newThread = newThreadRecord() - val mappings = OpenGroupMigrator.getExistingMappings(emptyList(), listOf(newThread)) - assertTrue(mappings.isEmpty()) - } - - @Test - fun `it should return null new thread in mappings if there are only legacy open groups`() { - val legacyThread = legacyThreadRecord() - val mappings = OpenGroupMigrator.getExistingMappings(listOf(legacyThread), emptyList()) - val expectedMappings = listOf( - OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, null) - ) - assertTrue(expectedMappings.containsAll(mappings)) - } - - @Test - fun `test migration thread DB calls legacy and returns if no legacy official groups`() { - val mockedThreadDb = mock { - on { legacyOxenOpenGroups } doReturn emptyList() - } - val mockedDbComponent = mock { - on { threadDatabase() } doReturn mockedThreadDb - } - - OpenGroupMigrator.migrate(mockedDbComponent) - - verify(mockedDbComponent).threadDatabase() - verify(mockedThreadDb).legacyOxenOpenGroups - verifyNoMoreInteractions(mockedThreadDb) - } - - @Test - fun `it should migrate on thread, group and loki dbs with correct values for legacy only migration`() { - // mock threadDB - val capturedThreadId = argumentCaptor() - val capturedNewEncoded = argumentCaptor() - val mockedThreadDb = mock { - val legacyThreadRecord = legacyThreadRecord() - on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord) - on { httpsOxenOpenGroups } doReturn emptyList() - on { migrateEncodedGroup(capturedThreadId.capture(), capturedNewEncoded.capture()) } doAnswer {} - } - - // mock groupDB - val capturedGroupLegacyEncoded = argumentCaptor() - val capturedGroupNewEncoded = argumentCaptor() - val mockedGroupDb = mock { - on { - migrateEncodedGroup( - capturedGroupLegacyEncoded.capture(), - capturedGroupNewEncoded.capture() - ) - } doAnswer {} - } - - // mock LokiAPIDB - val capturedLokiLegacyGroup = argumentCaptor() - val capturedLokiNewGroup = argumentCaptor() - val mockedLokiApi = mock { - on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {} - } - - val pubKey = OpenGroupApi.defaultServerPublicKey - val room = "oxen" - val legacyServer = OpenGroupApi.legacyDefaultServer - val newServer = OpenGroupApi.defaultServer - - val lokiThreadOpenGroup = argumentCaptor() - val mockedLokiThreadDb = mock { - on { getOpenGroupChat(eq(LEGACY_THREAD_ID)) } doReturn OpenGroup(legacyServer, room, "Oxen", 0, pubKey) - on { setOpenGroupChat(lokiThreadOpenGroup.capture(), eq(LEGACY_THREAD_ID)) } doAnswer {} - } - - val mockedDbComponent = mock { - on { threadDatabase() } doReturn mockedThreadDb - on { groupDatabase() } doReturn mockedGroupDb - on { lokiAPIDatabase() } doReturn mockedLokiApi - on { lokiThreadDatabase() } doReturn mockedLokiThreadDb - } - - OpenGroupMigrator.migrate(mockedDbComponent) - - // expect threadDB migration to reflect new thread values: - // thread ID = 1, encoded ID = new encoded ID - assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue) - assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedNewEncoded.firstValue) - - // expect groupDB migration to reflect new thread values: - // legacy encoded ID, new encoded ID - assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue) - assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedGroupNewEncoded.firstValue) - - // expect Loki API DB migration to reflect new thread values: - assertEquals("${OpenGroupApi.legacyDefaultServer}.oxen", capturedLokiLegacyGroup.firstValue) - assertEquals("${OpenGroupApi.defaultServer}.oxen", capturedLokiNewGroup.firstValue) - - assertEquals(newServer, lokiThreadOpenGroup.firstValue.server) - - } - - @Test - fun `it should migrate and delete legacy thread with conflicting new and old values`() { - - // mock threadDB - val capturedThreadId = argumentCaptor() - val mockedThreadDb = mock { - val legacyThreadRecord = legacyThreadRecord() - val newThreadRecord = newThreadRecord() - on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord) - on { httpsOxenOpenGroups } doReturn listOf(newThreadRecord) - on { deleteConversation(capturedThreadId.capture()) } doAnswer {} - } - - // mock groupDB - val capturedGroupLegacyEncoded = argumentCaptor() - val mockedGroupDb = mock { - on { delete(capturedGroupLegacyEncoded.capture()) } doReturn true - } - - // mock LokiAPIDB - val capturedLokiLegacyGroup = argumentCaptor() - val capturedLokiNewGroup = argumentCaptor() - val mockedLokiApi = mock { - on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {} - } - - // mock messaging dbs - val migrateMmsFromThreadId = argumentCaptor() - val migrateMmsToThreadId = argumentCaptor() - - val mockedMmsDb = mock { - on { migrateThreadId(migrateMmsFromThreadId.capture(), migrateMmsToThreadId.capture()) } doAnswer {} - } - - val migrateSmsFromThreadId = argumentCaptor() - val migrateSmsToThreadId = argumentCaptor() - val mockedSmsDb = mock { - on { migrateThreadId(migrateSmsFromThreadId.capture(), migrateSmsToThreadId.capture()) } doAnswer {} - } - - val lokiFromThreadId = argumentCaptor() - val lokiToThreadId = argumentCaptor() - val mockedLokiMessageDatabase = mock { - on { migrateThreadId(lokiFromThreadId.capture(), lokiToThreadId.capture()) } doAnswer {} - } - - val mockedLokiThreadDb = mock { - on { removeOpenGroupChat(eq(LEGACY_THREAD_ID)) } doAnswer {} - } - - val mockedDbComponent = mock { - on { threadDatabase() } doReturn mockedThreadDb - on { groupDatabase() } doReturn mockedGroupDb - on { lokiAPIDatabase() } doReturn mockedLokiApi - on { mmsDatabase() } doReturn mockedMmsDb - on { smsDatabase() } doReturn mockedSmsDb - on { lokiMessageDatabase() } doReturn mockedLokiMessageDatabase - on { lokiThreadDatabase() } doReturn mockedLokiThreadDb - } - - OpenGroupMigrator.migrate(mockedDbComponent) - - // should delete thread by thread ID - assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue) - - // should delete group by legacy encoded ID - assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue) - - // should migrate SMS from legacy thread ID to new thread ID - assertEquals(LEGACY_THREAD_ID, migrateSmsFromThreadId.firstValue) - assertEquals(NEW_THREAD_ID, migrateSmsToThreadId.firstValue) - - // should migrate MMS from legacy thread ID to new thread ID - assertEquals(LEGACY_THREAD_ID, migrateMmsFromThreadId.firstValue) - assertEquals(NEW_THREAD_ID, migrateMmsToThreadId.firstValue) - - } - - - -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 826df3ef8d..ef1d7567b3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -42,7 +42,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id" } - override fun execute() { + override fun execute(dispatcherName: String) { val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val threadID = storage.getThreadIdForMms(databaseMessageID) @@ -59,7 +59,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) Log.d("AttachmentDownloadJob", "Setting attachment state = failed, don't have attachment") messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID) } - this.handlePermanentFailure(exception) + this.handlePermanentFailure(dispatcherName, exception) } else if (exception == Error.DuplicateData) { attachment?.let { id -> Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data") @@ -68,7 +68,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data") messageDataProvider.setAttachmentState(AttachmentState.DONE, AttachmentId(attachmentID,0), databaseMessageID) } - this.handleSuccess() + this.handleSuccess(dispatcherName) } else { if (failureCount + 1 >= maxFailureCount) { attachment?.let { id -> @@ -79,7 +79,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID) } } - this.handleFailure(exception) + this.handleFailure(dispatcherName, exception) } } @@ -150,7 +150,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) Log.d("AttachmentDownloadJob", "deleting tempfile") tempFile.delete() Log.d("AttachmentDownloadJob", "succeeding job") - handleSuccess() + handleSuccess(dispatcherName) } catch (e: Exception) { Log.e("AttachmentDownloadJob", "Error processing attachment download", e) tempFile?.delete() @@ -169,17 +169,17 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) } } - private fun handleSuccess() { + private fun handleSuccess(dispatcherName: String) { Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.") - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) } - private fun handlePermanentFailure(e: Exception) { - delegate?.handleJobFailedPermanently(this, e) + private fun handlePermanentFailure(dispatcherName: String, e: Exception) { + delegate?.handleJobFailedPermanently(this, dispatcherName, e) } - private fun handleFailure(e: Exception) { - delegate?.handleJobFailed(this, e) + private fun handleFailure(dispatcherName: String, e: Exception) { + delegate?.handleJobFailed(this, dispatcherName, e) } private fun createTempFile(): File { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 360207af43..cd4189a653 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -45,29 +45,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id" } - override fun execute() { + override fun execute(dispatcherName: String) { try { val storage = MessagingModuleConfiguration.shared.storage val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID) - ?: return handleFailure(Error.NoAttachment) + ?: return handleFailure(dispatcherName, Error.NoAttachment) val openGroup = storage.getOpenGroup(threadID.toLong()) if (openGroup != null) { val keyAndResult = upload(attachment, openGroup.server, false) { OpenGroupApi.upload(it, openGroup.room, openGroup.server) } - handleSuccess(attachment, keyAndResult.first, keyAndResult.second) + handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } else { val keyAndResult = upload(attachment, FileServerApi.server, true) { FileServerApi.upload(it) } - handleSuccess(attachment, keyAndResult.first, keyAndResult.second) + handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } } catch (e: java.lang.Exception) { if (e == Error.NoAttachment) { - this.handlePermanentFailure(e) + this.handlePermanentFailure(dispatcherName, e) } else { - this.handleFailure(e) + this.handleFailure(dispatcherName, e) } } } @@ -104,9 +104,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess return Pair(key, UploadResult(id, "${server}/file/$id", digest)) } - private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) { + private fun handleSuccess(dispatcherName: String, attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) { Log.d(TAG, "Attachment uploaded successfully.") - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) if (attachment.contentType.startsWith("audio/")) { @@ -144,16 +144,16 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess storage.resumeMessageSendJobIfNeeded(messageSendJobID) } - private fun handlePermanentFailure(e: Exception) { + private fun handlePermanentFailure(dispatcherName: String, e: Exception) { Log.w(TAG, "Attachment upload failed permanently due to error: $this.") - delegate?.handleJobFailedPermanently(this, e) + delegate?.handleJobFailedPermanently(this, dispatcherName, e) MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID) failAssociatedMessageSendJob(e) } - private fun handleFailure(e: Exception) { + private fun handleFailure(dispatcherName: String, e: Exception) { Log.w(TAG, "Attachment upload failed due to error: $this.") - delegate?.handleJobFailed(this, e) + delegate?.handleJobFailed(this, dispatcherName, e) if (failureCount + 1 >= maxFailureCount) { failAssociatedMessageSendJob(e) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt index c679724b9f..ef67408fb3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt @@ -29,14 +29,14 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { return "$server.$room" } - override fun execute() { + override fun execute(dispatcherName: String) { try { val openGroup = OpenGroupUrlParser.parseUrl(joinUrl) val storage = MessagingModuleConfiguration.shared.storage val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL } if (allOpenGroups.contains(openGroup.joinUrl())) { Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException()) - delegate?.handleJobFailed(this, DuplicateGroupException()) + delegate?.handleJobFailed(this, dispatcherName, DuplicateGroupException()) return } // get image @@ -50,11 +50,11 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { storage.onOpenGroupAdded(openGroup.server) } catch (e: Exception) { Log.e("OpenGroupDispatcher", "Failed to add group because",e) - delegate?.handleJobFailed(this, e) + delegate?.handleJobFailed(this, dispatcherName, e) return } Log.d("Loki", "Group added successfully") - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) } override fun serialize(): Data = Data.Builder() diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 18a8cc4aed..54a6551dc4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -66,11 +66,11 @@ class BatchMessageReceiveJob( return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID) } - override fun execute() { - executeAsync().get() + override fun execute(dispatcherName: String) { + executeAsync(dispatcherName).get() } - fun executeAsync(): Promise { + fun executeAsync(dispatcherName: String): Promise { return task { val threadMap = mutableMapOf>() val storage = MessagingModuleConfiguration.shared.storage @@ -188,19 +188,21 @@ class BatchMessageReceiveJob( deferredThreadMap.awaitAll() } if (failures.isEmpty()) { - handleSuccess() + handleSuccess(dispatcherName) } else { - handleFailure() + handleFailure(dispatcherName) } } } - private fun handleSuccess() { - this.delegate?.handleJobSucceeded(this) + private fun handleSuccess(dispatcherName: String) { + Log.i(TAG, "Completed processing of ${messages.size} messages") + this.delegate?.handleJobSucceeded(this, dispatcherName) } - private fun handleFailure() { - this.delegate?.handleJobFailed(this, Exception("One or more jobs resulted in failure")) + private fun handleFailure(dispatcherName: String) { + Log.i(TAG, "Handling failure of ${failures.size} messages (${messages.size - failures.size} processed successfully)") + this.delegate?.handleJobFailed(this, dispatcherName, Exception("One or more jobs resulted in failure")) } override fun serialize(): Data { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt index 02f792117c..6429d760ae 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt @@ -12,7 +12,7 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job { override var failureCount: Int = 0 override val maxFailureCount: Int = 10 - override fun execute() { + override fun execute(dispatcherName: String) { val storage = MessagingModuleConfiguration.shared.storage val imageId = storage.getOpenGroup(room, server)?.imageId ?: return try { @@ -20,9 +20,9 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job { val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) storage.updateProfilePicture(groupId, bytes) storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) } catch (e: Exception) { - delegate?.handleJobFailed(this, e) + delegate?.handleJobFailed(this, dispatcherName, e) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt index 74feb83a61..74e324f0ea 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt @@ -17,7 +17,7 @@ interface Job { internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes } - fun execute() + fun execute(dispatcherName: String) fun serialize(): Data diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt index 535ea27f3c..769458ab6d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt @@ -2,7 +2,7 @@ package org.session.libsession.messaging.jobs interface JobDelegate { - fun handleJobSucceeded(job: Job) - fun handleJobFailed(job: Job, error: Exception) - fun handleJobFailedPermanently(job: Job, error: Exception) + fun handleJobSucceeded(job: Job, dispatcherName: String) + fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) + fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 8e46f275f2..b78590c729 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -53,7 +53,7 @@ class JobQueue : JobDelegate { } if (openGroupId.isNullOrEmpty()) { Log.e("OpenGroupDispatcher", "Open Group ID was null on ${job.javaClass.simpleName}") - handleJobFailedPermanently(job, NullPointerException("Open Group ID was null")) + handleJobFailedPermanently(job, name, NullPointerException("Open Group ID was null")) } else { val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) { Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel") @@ -95,9 +95,16 @@ class JobQueue : JobDelegate { } private fun Job.process(dispatcherName: String) { - Log.d(dispatcherName,"processJob: ${javaClass.simpleName}") + Log.d(dispatcherName,"processJob: ${javaClass.simpleName} (id: $id)") delegate = this@JobQueue - execute() + + try { + execute(dispatcherName) + } + catch (e: Exception) { + Log.d(dispatcherName, "unhandledJobException: ${javaClass.simpleName} (id: $id)") + this@JobQueue.handleJobFailed(this, dispatcherName, e) + } } init { @@ -177,7 +184,7 @@ class JobQueue : JobDelegate { return } if (!pendingJobIds.add(id)) { - Log.e("Loki","tried to re-queue pending/in-progress job") + Log.e("Loki","tried to re-queue pending/in-progress job (id: $id)") return } queue.trySend(job) @@ -196,7 +203,7 @@ class JobQueue : JobDelegate { } } pendingJobs.sortedBy { it.id }.forEach { job -> - Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName}.") + Log.i("Loki", "Resuming pending job of type: ${job::class.simpleName} (id: ${job.id}).") queue.trySend(job) // Offer always called on unlimited capacity } } @@ -223,21 +230,21 @@ class JobQueue : JobDelegate { } } - override fun handleJobSucceeded(job: Job) { + override fun handleJobSucceeded(job: Job, dispatcherName: String) { val jobId = job.id ?: return MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId) pendingJobIds.remove(jobId) } - override fun handleJobFailed(job: Job, error: Exception) { + override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) { // Canceled val storage = MessagingModuleConfiguration.shared.storage if (storage.isJobCanceled(job)) { - return Log.i("Loki", "${job::class.simpleName} canceled.") + return Log.i("Loki", "${job::class.simpleName} canceled (id: ${job.id}).") } // Message send jobs waiting for the attachment to upload if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) { - Log.i("Loki", "Message send job waiting for attachment upload to finish.") + Log.i("Loki", "Message send job waiting for attachment upload to finish (id: ${job.id}).") return } @@ -255,21 +262,22 @@ class JobQueue : JobDelegate { job.failureCount += 1 if (job.failureCount >= job.maxFailureCount) { - handleJobFailedPermanently(job, error) + handleJobFailedPermanently(job, dispatcherName, error) } else { storage.persistJob(job) val retryInterval = getRetryInterval(job) - Log.i("Loki", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).") + Log.i("Loki", "${job::class.simpleName} failed (id: ${job.id}); scheduling retry (failure count is ${job.failureCount}).") timer.schedule(delay = retryInterval) { - Log.i("Loki", "Retrying ${job::class.simpleName}.") + Log.i("Loki", "Retrying ${job::class.simpleName} (id: ${job.id}).") queue.trySend(job) } } } - override fun handleJobFailedPermanently(job: Job, error: Exception) { + override fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception) { val jobId = job.id ?: return handleJobFailedPermanently(jobId) + Log.d(dispatcherName, "permanentlyFailedJob: ${javaClass.simpleName} (id: ${job.id})") } private fun handleJobFailedPermanently(jobId: String) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index 439fbb7a3a..2ba33b5632 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -25,11 +25,11 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val private val OPEN_GROUP_ID_KEY = "open_group_id" } - override fun execute() { - executeAsync().get() + override fun execute(dispatcherName: String) { + executeAsync(dispatcherName).get() } - fun executeAsync(): Promise { + fun executeAsync(dispatcherName: String): Promise { val deferred = deferred() try { val isRetry: Boolean = failureCount != 0 @@ -39,32 +39,32 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey) message.serverHash = serverHash MessageReceiver.handle(message, proto, this.openGroupID) - this.handleSuccess() + this.handleSuccess(dispatcherName) deferred.resolve(Unit) } catch (e: Exception) { Log.e(TAG, "Couldn't receive message.", e) if (e is MessageReceiver.Error && !e.isRetryable) { Log.e("Loki", "Message receive job permanently failed.", e) - this.handlePermanentFailure(e) + this.handlePermanentFailure(dispatcherName, e) } else { Log.e("Loki", "Couldn't receive message.", e) - this.handleFailure(e) + this.handleFailure(dispatcherName, e) } deferred.resolve(Unit) // The promise is just used to keep track of when we're done } return deferred.promise } - private fun handleSuccess() { - delegate?.handleJobSucceeded(this) + private fun handleSuccess(dispatcherName: String) { + delegate?.handleJobSucceeded(this, dispatcherName) } - private fun handlePermanentFailure(e: Exception) { - delegate?.handleJobFailedPermanently(this, e) + private fun handlePermanentFailure(dispatcherName: String, e: Exception) { + delegate?.handleJobFailedPermanently(this, dispatcherName, e) } - private fun handleFailure(e: Exception) { - delegate?.handleJobFailed(this, e) + private fun handleFailure(dispatcherName: String, e: Exception) { + delegate?.handleJobFailed(this, dispatcherName, e) } override fun serialize(): Data { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 8ce1adf481..524338592c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -33,7 +33,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { private val DESTINATION_KEY = "destination" } - override fun execute() { + override fun execute(dispatcherName: String) { val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val message = message as? VisibleMessage val storage = MessagingModuleConfiguration.shared.storage @@ -61,12 +61,12 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { } } if (attachmentsToUpload.isNotEmpty()) { - this.handleFailure(AwaitingAttachmentUploadException) + this.handleFailure(dispatcherName, AwaitingAttachmentUploadException) return } // Wait for all attachments to upload before continuing } val promise = MessageSender.send(this.message, this.destination).success { - this.handleSuccess() + this.handleSuccess(dispatcherName) }.fail { exception -> var logStacktrace = true @@ -75,14 +75,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { is HTTP.HTTPRequestFailedException -> { logStacktrace = false - if (exception.statusCode == 429) { this.handlePermanentFailure(exception) } - else { this.handleFailure(exception) } + if (exception.statusCode == 429) { this.handlePermanentFailure(dispatcherName, exception) } + else { this.handleFailure(dispatcherName, exception) } } is MessageSender.Error -> { - if (!exception.isRetryable) { this.handlePermanentFailure(exception) } - else { this.handleFailure(exception) } + if (!exception.isRetryable) { this.handlePermanentFailure(dispatcherName, exception) } + else { this.handleFailure(dispatcherName, exception) } } - else -> this.handleFailure(exception) + else -> this.handleFailure(dispatcherName, exception) } if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) } @@ -95,15 +95,15 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { } } - private fun handleSuccess() { - delegate?.handleJobSucceeded(this) + private fun handleSuccess(dispatcherName: String) { + delegate?.handleJobSucceeded(this, dispatcherName) } - private fun handlePermanentFailure(error: Exception) { - delegate?.handleJobFailedPermanently(this, error) + private fun handlePermanentFailure(dispatcherName: String, error: Exception) { + delegate?.handleJobFailedPermanently(this, dispatcherName, error) } - private fun handleFailure(error: Exception) { + private fun handleFailure(dispatcherName: String, error: Exception) { Log.w(TAG, "Failed to send $message::class.simpleName.") val message = message as? VisibleMessage if (message != null) { @@ -111,7 +111,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { return // The message has been deleted } } - delegate?.handleJobFailed(this, error) + delegate?.handleJobFailed(this, dispatcherName, error) } override fun serialize(): Data { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 5c393c97b5..25fb2194c8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -32,7 +32,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { private val MESSAGE_KEY = "message" } - override fun execute() { + override fun execute(dispatcherName: String) { val server = PushNotificationAPI.server val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) val url = "${server}/notify" @@ -48,18 +48,18 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job { Log.d("Loki", "Couldn't notify PN server due to error: $exception.") } }.success { - handleSuccess() + handleSuccess(dispatcherName) }. fail { - handleFailure(it) + handleFailure(dispatcherName, it) } } - private fun handleSuccess() { - delegate?.handleJobSucceeded(this) + private fun handleSuccess(dispatcherName: String) { + delegate?.handleJobSucceeded(this, dispatcherName) } - private fun handleFailure(error: Exception) { - delegate?.handleJobFailed(this, error) + private fun handleFailure(dispatcherName: String, error: Exception) { + delegate?.handleJobFailed(this, dispatcherName, error) } override fun serialize(): Data { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt index 1fb2d0df22..4c76f87633 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt @@ -19,7 +19,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th override var failureCount: Int = 0 override val maxFailureCount: Int = 1 - override fun execute() { + override fun execute(dispatcherName: String) { val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider val numberToDelete = messageServerIds.size Log.d(TAG, "Deleting $numberToDelete messages") @@ -39,10 +39,10 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th } Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully") - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) } catch (e: Exception) { - delegate?.handleJobFailed(this, e) + delegate?.handleJobFailed(this, dispatcherName, e) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt index e02a2f00e5..d082ac7088 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt @@ -20,7 +20,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job { const val THREAD_LENGTH_TRIGGER_SIZE = 2000 } - override fun execute() { + override fun execute(dispatcherName: String) { val context = MessagingModuleConfiguration.shared.context val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context) val storage = MessagingModuleConfiguration.shared.storage @@ -29,7 +29,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job { val oldestMessageTime = System.currentTimeMillis() - TRIM_TIME_LIMIT storage.trimThreadBefore(threadId, oldestMessageTime) } - delegate?.handleJobSucceeded(this) + delegate?.handleJobSucceeded(this, dispatcherName) } override fun serialize(): Data { From 50989cb2eeea76433e6583fc232d372676bca5a9 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 6 Feb 2023 14:22:26 +1100 Subject: [PATCH 2/3] Increased file upload limits to 10Mb --- .../securesms/mms/PushMediaConstraints.java | 10 +++++----- .../libsession/messaging/file_server/FileServerApi.kt | 9 --------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java index 179c28bc3c..22af450aa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java @@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints { @Override public int getImageMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getGifMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getVideoMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getAudioMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } @Override public int getDocumentMaxSize(Context context) { - return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier); + return FileServerApi.maxFileSize; } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 01fae1f503..0e8768d530 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -16,15 +16,6 @@ object FileServerApi { private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59" const val server = "http://filev2.getsession.org" const val maxFileSize = 10_000_000 // 10 MB - /** - * The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes - * is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP - * request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also - * be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when - * uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only - * possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. - */ - const val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5? sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") From 395ada62fffa7031244cd0a6f144a20e7e824562 Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Mon, 6 Feb 2023 16:01:16 +1100 Subject: [PATCH 3/3] Update French translations (#1103) * chore: update french translations file from crowdin * chore: update french translations libsession file from crowdin * chore: update french non-region translations file from crowdin * chore: update french non-region libsession translations file from crowdin --- app/src/main/res/values-fr-rFR/strings.xml | 213 +++++++++++++++--- app/src/main/res/values-fr/strings.xml | 213 +++++++++++++++--- .../src/main/res/values-fr-rFR/strings.xml | 54 +++++ libsession/src/main/res/values-fr/strings.xml | 54 +++++ 4 files changed, 474 insertions(+), 60 deletions(-) create mode 100644 libsession/src/main/res/values-fr-rFR/strings.xml create mode 100644 libsession/src/main/res/values-fr/strings.xml diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 9d58ab9178..3aeeba0550 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -1,10 +1,10 @@ - Session Oui Non Supprimer Bannir + Veuillez patienter… Enregistrer Note à mon intention Version %s @@ -19,7 +19,7 @@ Supprimer tous les anciens messages maintenant ? - Cela va immédiatement réduire toutes les conversations pour qu’il ne reste que le message le plus récent. + Cela réduira immédiatement toutes les conversations au message le plus récent. Cela réduira immédiatement toutes les conversations aux %d messages les plus récents. Supprimer @@ -63,6 +63,7 @@ Son désactivé jusqu\'à %1$s En sourdine %1$d membres + %1$d membres actifs Règles de la communauté Le destinataire est invalide ! Ajouté à l’écran d’accueil @@ -82,7 +83,9 @@ Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos. Session a besoin de l’autorisation Appareil photo afin de prendre des photos ou des vidéos, mais elle a été refusée définitivement. Veuillez accéder au menu des paramètres des applis, sélectionner Autorisations et activer Appareil photo. Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos - %1$d de %2$d + %1$d sur %2$d + Autorisations d\'appel requises + Vous pouvez activer la permission \"Appels vocaux et vidéo\" dans les paramètres de confidentialité. Supprimer le message sélectionné ? @@ -99,13 +102,17 @@ L’enregistrement des %1$d médias dans la mémoire permettra à n’importe quelles autres applis de votre appareil d’y accéder.\n\nContinuer ? - Erreur d’enregistrement de la pièce jointe dans la mémoire ! + Erreur lors de l’enregistrement de la pièce jointe dans la mémoire ! Erreur d’enregistrement des pièces jointes dans la mémoire ! Enregistrement de la pièce jointe Enregistrement de %1$d pièces jointes + + Enregistrement de la pièce jointe dans la mémoire… + Enregistrement de %1$d pièces jointes dans la mémoire… + Photo de profil @@ -156,7 +163,7 @@ Envoyer à %s - Ajouter un légende… + Ajouter une légende... Un élément a été supprimé, car il dépassait la taille limite L’appareil photo n’est pas disponible Message à %s @@ -191,6 +198,7 @@ Vidéo Vous avez reçu un message d’échange de clés corrompu ! + Le message d\'échange de clé reçu est pour une version du protocole invalide. Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher. Vous avez réinitialisé la session sécurisée. %s a réinitialisé la session sécurisée. @@ -201,7 +209,7 @@ La session sécurisée a été réinitialisée. Brouillon : Vous avez appelé - Vous a appelé + Vous a appelé·e Appel manqué Message multimédia %s est sur Session ! @@ -304,7 +312,7 @@ Envoyer Rédaction d’un message Afficher, masquer le clavier des émojis - Imagette de pièces jointes + Vignette de pièce jointe Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution Enregistrer et envoyer une pièce jointe audio Verrouiller l’enregistrement de pièces jointes audio @@ -378,9 +386,9 @@ Valeur par défaut Activé Désactivé - Nom et message - Nom seulement - Aucun nom ni message + Nom et Contenu + Nom uniquement + Aucun nom ni contenu Images Son Vidéo @@ -398,9 +406,14 @@ %d heures - La touche Entrée envoie - Envoyer des aperçus de liens - Les aperçus sont pris en charge pour les liens Imgur, Instagram, Pinterest, Reddit et YouTube + Envoyer avec bouton Entrée + Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne. + Envoyer les aperçus des liens + Aperçus des liens + Générer les aperçus des liens pour les URLs supportés. + Messages audio + Lire automatiquement les messages audios + Lire automatiquement les messages audio consécutifs. Sécurité de l’écran Bloquer les captures d’écran dans la liste des récents et dans l’appli Notifications @@ -408,6 +421,7 @@ Inconnue Rythme de clignotement de la DEL Son + Son à l\'ouverture de l\'application Silencieux Répéter les alertes Jamais @@ -436,18 +450,27 @@ Valeur par défaut Clavier incognito Accusés de lecture + Envoyer des accusés de lecture dans les conversations individuelles. Indicateurs de saisie - Si les indicateurs de saisie sont désactivés, vous ne serez pas en mesure de voir les indicateurs de saisie des autres. + Voir et envoyer les indicateurs de saisie dans les conversations un à un. Demander au clavier de désactiver l’apprentissage personnalisé Clair Sombre Élagage des messages + Raccourcir les communautés + Supprimer des messages de plus de 6 mois dans des communautés qui ont plus de 2 000 messages. Utiliser les émojis du système Désactiver la prise en charge des émojis intégrés à Session + Sécurité d\'écran Conversations Messages Sons des conversations + Contenu de la notification + Afficher : + Informations affichées dans les notifications. Priorité + Notifications de capture d\'écran + Recevoir une notification lorsqu\'un contact prend une capture d\'écran d\'une conversation individuelle. @@ -460,7 +483,10 @@ Bannir l\'utilisateur Bannir et supprimer tout Renvoyer le message + Répondre Répondre au message + Appeler + Sélectionner Enregistrer la pièce jointe @@ -513,8 +539,8 @@ Création de la sauvegarde… %d messages pour l’instant Jamais - Verrouillage de l’écran - Verrouiller l’accès à Session avec le verrouillage de l’écran d’Android ou une empreinte + Verrouiller Session + Nécessite une empreinte digitale, un code PIN, un schéma ou un mot de passe pour déverrouiller Session. Délai d’inactivité avant verrouillage de l’écran Aucune @@ -522,6 +548,7 @@ Continuer Copier + Fermer URL non valide Copié dans le presse-papier Suivant @@ -573,12 +600,12 @@ Contact en cours… Nouvelle Session Saisir un Session ID - Scanner un Code QR - Scannez le code QR d\'un utilisateur pour démarrer une session. Les codes QR peuvent se trouver en touchant l\'icône du code QR dans les paramètres du compte. + Scanner un QR Code + Scannez le QR code d\'un utilisateur pour démarrer une session. Les QR codes peuvent se trouver en touchant l\'icône du QR code dans les paramètres du compte. Entrer un Session ID ou un nom ONS Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR. Veuillez vérifier le Session ID ou le nom ONS et réessayer. - Session a besoin d\'accéder à l\'appareil photo pour scanner les codes QR + Session a besoin d\'accéder à l\'appareil photo pour scanner les QR codes Autoriser l\'accès Nouveau groupe privé Saisissez un nom de groupe @@ -591,7 +618,7 @@ Joindre un groupe public Impossible de rejoindre le groupe URL du groupe public - Scannez le code QR + Scanner le QR Code Scannez le code QR du groupe public que vous souhaitez rejoindre Saisissez une URL de groupe public Paramètres @@ -600,6 +627,7 @@ Veuillez choisir un nom d\'utilisateur plus court Confidentialité Notifications + Demandes de message Conversations Appareils reliés Inviter un ami @@ -612,25 +640,39 @@ Style de notification Contenu de notification Confidentialité + Conversations + Aide + Signaler un bug + Exportez vos logs, puis télécharger le fichier au service d\'aide de Session. + Traduire Session + Nous aimerions avoir votre avis + FAQ + Assistance + Exporter les journaux Stratégie de notification Utiliser le Mode Rapide Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google. Modifier le nom Déconnecter l\'appareil Votre phrase de récupération - Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil. + Vous pouvez utiliser votre phrase de récupération pour restaurer votre compte ou relier un appareil. Effacer toutes les données Cela supprimera définitivement vos messages, vos sessions et vos contacts. Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ? + Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ? + Effacer l\'appareil uniquement + Effacer l\'appareil et le réseau + Êtes-vous sûr de vouloir supprimer vos données du réseau ? Si vous continuez, vous ne pourrez pas restaurer vos messages ou vos contacts. + Effacer Effacer seulement Compte complet - Code QR - Afficher mon code QR - Scanner le code QR - Scannez le code QR d\'un autre utilisateur pour démarrer une session + QR Code + Afficher mon QR code + Scanner le QR Code + Scannez le QR code d\'un autre utilisateur pour démarrer une session Scannez-moi - Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous. - Partager le code QR + Ceci est votre QR code. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous. + Partager le QR code Contacts Groupes privés Groupes publics @@ -664,8 +706,8 @@ Cela prend un certain temps, voulez-vous passer ? Relier un appareil Phrase de récupération - Scannez le code QR - Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre code QR. + Scanner le QR Code + Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre QR code. Ou rejoignez l\'un(e) de ceux-ci… Notifications de message Session peut vous avertir de la présence de nouveaux messages de deux façons. @@ -694,6 +736,7 @@ Êtes-vous sûr de vouloir télécharger le média envoyé par %s ? Télécharger %s est bloqué. Débloquer ? + Bloquer l\'utilisateur La préparation de la pièce jointe pour l\'envoi a échoué. Médias Touchez pour télécharger %s @@ -711,6 +754,116 @@ Journal de débogage Partager les logs Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ? - Code pin + Épingler Désépingler + Tout marquer comme lu + Contacts et Groupes + Messages + Demandes de message + Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message et révélera votre ID de session. + Accepter + Refuser + Effacer tout + Êtes-vous sûr de vouloir refuser cette demande de message ? + Êtes-vous sûr de vouloir supprimer cette demande de message ? + Demande de message supprimée + Êtes-vous sûr de vouloir supprimer toutes les demandes de message ? + Demandes de message supprimées + Votre demande de message a été acceptée. + Votre demande de message est en attente. + Aucune demande de message en attente + Message privé + Groupes privés + Groupe public + Vous avez une nouvelle demande de message + Connexion… + Appel entrant + Refuser l’appel + Répondre à l’appel + Appel en cours + Annuler l’appel + Établissement de l\'appel + Raccrocher + Accepter l\'appel + Refuser l\'appel + Appels vocaux et vidéos + Appels (Bêta) + Active les appels vocaux et vidéo vers et depuis d\'autres utilisateurs. + Appels vocaux / vidéo + La version actuelle des appels vocaux/vidéo exposera votre adresse IP aux serveurs de la Fondation Oxen et aux utilisateurs appelés + Appel Manqué + Vous avez manqué un appel car vous devez activer la permission « Appels vocaux et vidéo » dans les paramètres de confidentialité. + Appel Session + Reconnexion… + Notifications + Les notifications désactivées vous empêcheront de recevoir des appels, aller dans les paramètres de notification de session? + Rejeter + Conversations + Apparence + Aide + Thèmes + Océan sombre + Sombre classique + Océan lumineux + Clair classique + Couleur principale + Ce message + Fréquemment Utilisés + Émoticônes et personnes + Nature + Nourriture + Activités + Voyage + Objets + Symboles + Drapeaux + Emoticônes + Aucun résultat trouvé + + Tous · %1$d + + +%1$d + + Vous + %1$s a réagi à un message %2$s + Masquer les détails + Rechercher un émoticône + Retour à l\'émoticône + Effacer la recherche + Thème sombre automatique + Faire correspondre aux paramètres systèmes + Accédez aux paramètres de notifications de l\'appareil + Contacts bloqués + Vous n\'avez aucun contact bloqué + Débloquer %s + Débloquer les utilisateurs + Êtes-vous sûr·e de vouloir débloquer %s ? + + et %d autre + et %d autres + + + Et %1$d autre a réagi %2$s à ce message + Et %1$d autres ont réagi %2$s à ce message + + Nouvelle conversation + Nouveau message + Créer un groupe + Rejoindre la communauté + Contacts + Inconnu·e + Commencez une nouvelle conversation en entrant l\'ID Session de quelqu\'un ou en lui partageant votre ID Session. + Créer + Rechercher parmi les contacts + URL de la communauté + Entrez l\'URL de la communauté + Rejoindre + Revenir en arrière + Fermer la fenêtre + Échec de la mise à jour de la base de données + Veuillez contacter le support pour signaler l\'erreur. + Envoi + Lu + Envoyé + Échec d’envoi diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9d58ab9178..3aeeba0550 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,10 +1,10 @@ - Session Oui Non Supprimer Bannir + Veuillez patienter… Enregistrer Note à mon intention Version %s @@ -19,7 +19,7 @@ Supprimer tous les anciens messages maintenant ? - Cela va immédiatement réduire toutes les conversations pour qu’il ne reste que le message le plus récent. + Cela réduira immédiatement toutes les conversations au message le plus récent. Cela réduira immédiatement toutes les conversations aux %d messages les plus récents. Supprimer @@ -63,6 +63,7 @@ Son désactivé jusqu\'à %1$s En sourdine %1$d membres + %1$d membres actifs Règles de la communauté Le destinataire est invalide ! Ajouté à l’écran d’accueil @@ -82,7 +83,9 @@ Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos. Session a besoin de l’autorisation Appareil photo afin de prendre des photos ou des vidéos, mais elle a été refusée définitivement. Veuillez accéder au menu des paramètres des applis, sélectionner Autorisations et activer Appareil photo. Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos - %1$d de %2$d + %1$d sur %2$d + Autorisations d\'appel requises + Vous pouvez activer la permission \"Appels vocaux et vidéo\" dans les paramètres de confidentialité. Supprimer le message sélectionné ? @@ -99,13 +102,17 @@ L’enregistrement des %1$d médias dans la mémoire permettra à n’importe quelles autres applis de votre appareil d’y accéder.\n\nContinuer ? - Erreur d’enregistrement de la pièce jointe dans la mémoire ! + Erreur lors de l’enregistrement de la pièce jointe dans la mémoire ! Erreur d’enregistrement des pièces jointes dans la mémoire ! Enregistrement de la pièce jointe Enregistrement de %1$d pièces jointes + + Enregistrement de la pièce jointe dans la mémoire… + Enregistrement de %1$d pièces jointes dans la mémoire… + Photo de profil @@ -156,7 +163,7 @@ Envoyer à %s - Ajouter un légende… + Ajouter une légende... Un élément a été supprimé, car il dépassait la taille limite L’appareil photo n’est pas disponible Message à %s @@ -191,6 +198,7 @@ Vidéo Vous avez reçu un message d’échange de clés corrompu ! + Le message d\'échange de clé reçu est pour une version du protocole invalide. Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher. Vous avez réinitialisé la session sécurisée. %s a réinitialisé la session sécurisée. @@ -201,7 +209,7 @@ La session sécurisée a été réinitialisée. Brouillon : Vous avez appelé - Vous a appelé + Vous a appelé·e Appel manqué Message multimédia %s est sur Session ! @@ -304,7 +312,7 @@ Envoyer Rédaction d’un message Afficher, masquer le clavier des émojis - Imagette de pièces jointes + Vignette de pièce jointe Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution Enregistrer et envoyer une pièce jointe audio Verrouiller l’enregistrement de pièces jointes audio @@ -378,9 +386,9 @@ Valeur par défaut Activé Désactivé - Nom et message - Nom seulement - Aucun nom ni message + Nom et Contenu + Nom uniquement + Aucun nom ni contenu Images Son Vidéo @@ -398,9 +406,14 @@ %d heures - La touche Entrée envoie - Envoyer des aperçus de liens - Les aperçus sont pris en charge pour les liens Imgur, Instagram, Pinterest, Reddit et YouTube + Envoyer avec bouton Entrée + Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne. + Envoyer les aperçus des liens + Aperçus des liens + Générer les aperçus des liens pour les URLs supportés. + Messages audio + Lire automatiquement les messages audios + Lire automatiquement les messages audio consécutifs. Sécurité de l’écran Bloquer les captures d’écran dans la liste des récents et dans l’appli Notifications @@ -408,6 +421,7 @@ Inconnue Rythme de clignotement de la DEL Son + Son à l\'ouverture de l\'application Silencieux Répéter les alertes Jamais @@ -436,18 +450,27 @@ Valeur par défaut Clavier incognito Accusés de lecture + Envoyer des accusés de lecture dans les conversations individuelles. Indicateurs de saisie - Si les indicateurs de saisie sont désactivés, vous ne serez pas en mesure de voir les indicateurs de saisie des autres. + Voir et envoyer les indicateurs de saisie dans les conversations un à un. Demander au clavier de désactiver l’apprentissage personnalisé Clair Sombre Élagage des messages + Raccourcir les communautés + Supprimer des messages de plus de 6 mois dans des communautés qui ont plus de 2 000 messages. Utiliser les émojis du système Désactiver la prise en charge des émojis intégrés à Session + Sécurité d\'écran Conversations Messages Sons des conversations + Contenu de la notification + Afficher : + Informations affichées dans les notifications. Priorité + Notifications de capture d\'écran + Recevoir une notification lorsqu\'un contact prend une capture d\'écran d\'une conversation individuelle. @@ -460,7 +483,10 @@ Bannir l\'utilisateur Bannir et supprimer tout Renvoyer le message + Répondre Répondre au message + Appeler + Sélectionner Enregistrer la pièce jointe @@ -513,8 +539,8 @@ Création de la sauvegarde… %d messages pour l’instant Jamais - Verrouillage de l’écran - Verrouiller l’accès à Session avec le verrouillage de l’écran d’Android ou une empreinte + Verrouiller Session + Nécessite une empreinte digitale, un code PIN, un schéma ou un mot de passe pour déverrouiller Session. Délai d’inactivité avant verrouillage de l’écran Aucune @@ -522,6 +548,7 @@ Continuer Copier + Fermer URL non valide Copié dans le presse-papier Suivant @@ -573,12 +600,12 @@ Contact en cours… Nouvelle Session Saisir un Session ID - Scanner un Code QR - Scannez le code QR d\'un utilisateur pour démarrer une session. Les codes QR peuvent se trouver en touchant l\'icône du code QR dans les paramètres du compte. + Scanner un QR Code + Scannez le QR code d\'un utilisateur pour démarrer une session. Les QR codes peuvent se trouver en touchant l\'icône du QR code dans les paramètres du compte. Entrer un Session ID ou un nom ONS Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR. Veuillez vérifier le Session ID ou le nom ONS et réessayer. - Session a besoin d\'accéder à l\'appareil photo pour scanner les codes QR + Session a besoin d\'accéder à l\'appareil photo pour scanner les QR codes Autoriser l\'accès Nouveau groupe privé Saisissez un nom de groupe @@ -591,7 +618,7 @@ Joindre un groupe public Impossible de rejoindre le groupe URL du groupe public - Scannez le code QR + Scanner le QR Code Scannez le code QR du groupe public que vous souhaitez rejoindre Saisissez une URL de groupe public Paramètres @@ -600,6 +627,7 @@ Veuillez choisir un nom d\'utilisateur plus court Confidentialité Notifications + Demandes de message Conversations Appareils reliés Inviter un ami @@ -612,25 +640,39 @@ Style de notification Contenu de notification Confidentialité + Conversations + Aide + Signaler un bug + Exportez vos logs, puis télécharger le fichier au service d\'aide de Session. + Traduire Session + Nous aimerions avoir votre avis + FAQ + Assistance + Exporter les journaux Stratégie de notification Utiliser le Mode Rapide Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google. Modifier le nom Déconnecter l\'appareil Votre phrase de récupération - Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil. + Vous pouvez utiliser votre phrase de récupération pour restaurer votre compte ou relier un appareil. Effacer toutes les données Cela supprimera définitivement vos messages, vos sessions et vos contacts. Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ? + Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ? + Effacer l\'appareil uniquement + Effacer l\'appareil et le réseau + Êtes-vous sûr de vouloir supprimer vos données du réseau ? Si vous continuez, vous ne pourrez pas restaurer vos messages ou vos contacts. + Effacer Effacer seulement Compte complet - Code QR - Afficher mon code QR - Scanner le code QR - Scannez le code QR d\'un autre utilisateur pour démarrer une session + QR Code + Afficher mon QR code + Scanner le QR Code + Scannez le QR code d\'un autre utilisateur pour démarrer une session Scannez-moi - Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous. - Partager le code QR + Ceci est votre QR code. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous. + Partager le QR code Contacts Groupes privés Groupes publics @@ -664,8 +706,8 @@ Cela prend un certain temps, voulez-vous passer ? Relier un appareil Phrase de récupération - Scannez le code QR - Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre code QR. + Scanner le QR Code + Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre QR code. Ou rejoignez l\'un(e) de ceux-ci… Notifications de message Session peut vous avertir de la présence de nouveaux messages de deux façons. @@ -694,6 +736,7 @@ Êtes-vous sûr de vouloir télécharger le média envoyé par %s ? Télécharger %s est bloqué. Débloquer ? + Bloquer l\'utilisateur La préparation de la pièce jointe pour l\'envoi a échoué. Médias Touchez pour télécharger %s @@ -711,6 +754,116 @@ Journal de débogage Partager les logs Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ? - Code pin + Épingler Désépingler + Tout marquer comme lu + Contacts et Groupes + Messages + Demandes de message + Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message et révélera votre ID de session. + Accepter + Refuser + Effacer tout + Êtes-vous sûr de vouloir refuser cette demande de message ? + Êtes-vous sûr de vouloir supprimer cette demande de message ? + Demande de message supprimée + Êtes-vous sûr de vouloir supprimer toutes les demandes de message ? + Demandes de message supprimées + Votre demande de message a été acceptée. + Votre demande de message est en attente. + Aucune demande de message en attente + Message privé + Groupes privés + Groupe public + Vous avez une nouvelle demande de message + Connexion… + Appel entrant + Refuser l’appel + Répondre à l’appel + Appel en cours + Annuler l’appel + Établissement de l\'appel + Raccrocher + Accepter l\'appel + Refuser l\'appel + Appels vocaux et vidéos + Appels (Bêta) + Active les appels vocaux et vidéo vers et depuis d\'autres utilisateurs. + Appels vocaux / vidéo + La version actuelle des appels vocaux/vidéo exposera votre adresse IP aux serveurs de la Fondation Oxen et aux utilisateurs appelés + Appel Manqué + Vous avez manqué un appel car vous devez activer la permission « Appels vocaux et vidéo » dans les paramètres de confidentialité. + Appel Session + Reconnexion… + Notifications + Les notifications désactivées vous empêcheront de recevoir des appels, aller dans les paramètres de notification de session? + Rejeter + Conversations + Apparence + Aide + Thèmes + Océan sombre + Sombre classique + Océan lumineux + Clair classique + Couleur principale + Ce message + Fréquemment Utilisés + Émoticônes et personnes + Nature + Nourriture + Activités + Voyage + Objets + Symboles + Drapeaux + Emoticônes + Aucun résultat trouvé + + Tous · %1$d + + +%1$d + + Vous + %1$s a réagi à un message %2$s + Masquer les détails + Rechercher un émoticône + Retour à l\'émoticône + Effacer la recherche + Thème sombre automatique + Faire correspondre aux paramètres systèmes + Accédez aux paramètres de notifications de l\'appareil + Contacts bloqués + Vous n\'avez aucun contact bloqué + Débloquer %s + Débloquer les utilisateurs + Êtes-vous sûr·e de vouloir débloquer %s ? + + et %d autre + et %d autres + + + Et %1$d autre a réagi %2$s à ce message + Et %1$d autres ont réagi %2$s à ce message + + Nouvelle conversation + Nouveau message + Créer un groupe + Rejoindre la communauté + Contacts + Inconnu·e + Commencez une nouvelle conversation en entrant l\'ID Session de quelqu\'un ou en lui partageant votre ID Session. + Créer + Rechercher parmi les contacts + URL de la communauté + Entrez l\'URL de la communauté + Rejoindre + Revenir en arrière + Fermer la fenêtre + Échec de la mise à jour de la base de données + Veuillez contacter le support pour signaler l\'erreur. + Envoi + Lu + Envoyé + Échec d’envoi diff --git a/libsession/src/main/res/values-fr-rFR/strings.xml b/libsession/src/main/res/values-fr-rFR/strings.xml new file mode 100644 index 0000000000..c8324290f7 --- /dev/null +++ b/libsession/src/main/res/values-fr-rFR/strings.xml @@ -0,0 +1,54 @@ + + + + Vous avez quitté le groupe. + Vous avez créé un nouveau groupe. + %1$s vous a ajouté·e dans le groupe. + Vous avez renommé le groupe en %1$s + %1$s a renommé le groupe en : %2$s + Vous avez ajouté %1$s au groupe. + %1$s a ajouté %2$s au groupe. + Vous avez retiré %1$s du groupe. + %1$s a supprimé %2$s du groupe. + Vous avez été retiré·e du groupe. + Vous + %s vous a appelé·e + Vous avez appelé %s + Appel manqué de %s + Vous avez désactivé les messages éphémères. + %1$s a désactivé les messages éphémères. + Vous avez défini l’expiration des messages éphémères à %1$s + %1$s a défini l’expiration des messages éphémères à %2$s + %1$s a pris une capture d\'écran. + %1$s a enregistré le média. + + Désactivé + + %d seconde + %d secondes + + %d s + + %d minute + %d minutes + + %d min + + %d heure + %d heures + + %d h + + %d jour + %d jours + + %d j + + %d semaine + %d semaines + + %d sem + %1$s a quitté le groupe. + + Groupe sans nom + diff --git a/libsession/src/main/res/values-fr/strings.xml b/libsession/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000000..c8324290f7 --- /dev/null +++ b/libsession/src/main/res/values-fr/strings.xml @@ -0,0 +1,54 @@ + + + + Vous avez quitté le groupe. + Vous avez créé un nouveau groupe. + %1$s vous a ajouté·e dans le groupe. + Vous avez renommé le groupe en %1$s + %1$s a renommé le groupe en : %2$s + Vous avez ajouté %1$s au groupe. + %1$s a ajouté %2$s au groupe. + Vous avez retiré %1$s du groupe. + %1$s a supprimé %2$s du groupe. + Vous avez été retiré·e du groupe. + Vous + %s vous a appelé·e + Vous avez appelé %s + Appel manqué de %s + Vous avez désactivé les messages éphémères. + %1$s a désactivé les messages éphémères. + Vous avez défini l’expiration des messages éphémères à %1$s + %1$s a défini l’expiration des messages éphémères à %2$s + %1$s a pris une capture d\'écran. + %1$s a enregistré le média. + + Désactivé + + %d seconde + %d secondes + + %d s + + %d minute + %d minutes + + %d min + + %d heure + %d heures + + %d h + + %d jour + %d jours + + %d j + + %d semaine + %d semaines + + %d sem + %1$s a quitté le groupe. + + Groupe sans nom +