mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 02:08:33 +00:00
Merge remote-tracking branch 'upstream/dev' into libsession-integration
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java # libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt # libsession/src/main/java/org/session/libsession/messaging/jobs/TrimThreadJob.kt
This commit is contained in:
commit
1b580cca1b
@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
|||||||
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
|
||||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
import org.thoughtcrime.securesms.groups.OpenGroupManager;
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity;
|
import org.thoughtcrime.securesms.home.HomeActivity;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
||||||
@ -217,9 +216,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
|
|||||||
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this),
|
||||||
configFactory
|
configFactory
|
||||||
);
|
);
|
||||||
// migrate session open group data
|
|
||||||
OpenGroupMigrator.migrate(getDatabaseComponent());
|
|
||||||
// end migration
|
|
||||||
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
|
||||||
Log.i(TAG, "onCreate()");
|
Log.i(TAG, "onCreate()");
|
||||||
startKovenant();
|
startKovenant();
|
||||||
|
@ -210,8 +210,7 @@ public class PassphrasePromptActivity extends BaseActionBarActivity {
|
|||||||
try {
|
try {
|
||||||
signature = biometricSecretProvider.getOrCreateBiometricSignature(this);
|
signature = biometricSecretProvider.getOrCreateBiometricSignature(this);
|
||||||
hasSignatureObject = true;
|
hasSignatureObject = true;
|
||||||
throw new InvalidKeyException("e");
|
} catch (Exception e) {
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
signature = null;
|
signature = null;
|
||||||
hasSignatureObject = false;
|
hasSignatureObject = false;
|
||||||
Log.e(TAG, "Error getting / creating signature", e);
|
Log.e(TAG, "Error getting / creating signature", e);
|
||||||
|
@ -4,30 +4,48 @@ import android.graphics.Bitmap;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import com.bumptech.glide.request.target.BitmapImageViewTarget;
|
import com.bumptech.glide.request.target.BitmapImageViewTarget;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
|
public class GlideBitmapListeningTarget extends BitmapImageViewTarget {
|
||||||
|
|
||||||
private final SettableFuture<Boolean> loaded;
|
private final SettableFuture<Boolean> loaded;
|
||||||
|
private final WeakReference<View> loadingView;
|
||||||
|
|
||||||
public GlideBitmapListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
|
public GlideBitmapListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
|
||||||
super(view);
|
super(view);
|
||||||
this.loaded = loaded;
|
this.loaded = loaded;
|
||||||
|
this.loadingView = new WeakReference<View>(loadingView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setResource(@Nullable Bitmap resource) {
|
protected void setResource(@Nullable Bitmap resource) {
|
||||||
super.setResource(resource);
|
super.setResource(resource);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||||
super.onLoadFailed(errorDrawable);
|
super.onLoadFailed(errorDrawable);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,30 +3,48 @@ package org.thoughtcrime.securesms.components;
|
|||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
|
||||||
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
import com.bumptech.glide.request.target.DrawableImageViewTarget;
|
||||||
|
|
||||||
import org.session.libsignal.utilities.SettableFuture;
|
import org.session.libsignal.utilities.SettableFuture;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
|
public class GlideDrawableListeningTarget extends DrawableImageViewTarget {
|
||||||
|
|
||||||
private final SettableFuture<Boolean> loaded;
|
private final SettableFuture<Boolean> loaded;
|
||||||
|
private final WeakReference<View> loadingView;
|
||||||
|
|
||||||
public GlideDrawableListeningTarget(@NonNull ImageView view, @NonNull SettableFuture<Boolean> loaded) {
|
public GlideDrawableListeningTarget(@NonNull ImageView view, @Nullable View loadingView, @NonNull SettableFuture<Boolean> loaded) {
|
||||||
super(view);
|
super(view);
|
||||||
this.loaded = loaded;
|
this.loaded = loaded;
|
||||||
|
this.loadingView = new WeakReference<View>(loadingView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setResource(@Nullable Drawable resource) {
|
protected void setResource(@Nullable Drawable resource) {
|
||||||
super.setResource(resource);
|
super.setResource(resource);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||||
super.onLoadFailed(errorDrawable);
|
super.onLoadFailed(errorDrawable);
|
||||||
loaded.set(true);
|
loaded.set(true);
|
||||||
|
|
||||||
|
View loadingViewInstance = loadingView.get();
|
||||||
|
|
||||||
|
if (loadingViewInstance != null) {
|
||||||
|
loadingViewInstance.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1007,6 +1007,18 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
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) {
|
override fun showExpiringMessagesDialog(thread: Recipient) {
|
||||||
if (thread.isClosedGroupRecipient) {
|
if (thread.isClosedGroupRecipient) {
|
||||||
val group = groupDb.getGroup(thread.address.toGroupString()).orNull()
|
val group = groupDb.getGroup(thread.address.toGroupString()).orNull()
|
||||||
@ -1471,16 +1483,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
|
val hasSeenGIFMetaDataWarning: Boolean = textSecurePreferences.hasSeenGIFMetaDataWarning()
|
||||||
if (!hasSeenGIFMetaDataWarning) {
|
if (!hasSeenGIFMetaDataWarning) {
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
builder.setTitle("Search GIFs?")
|
builder.setTitle(R.string.giphy_permission_title)
|
||||||
builder.setMessage("You will not have full metadata protection when sending GIFs.")
|
builder.setMessage(R.string.giphy_permission_message)
|
||||||
builder.setPositiveButton("OK") { dialog: DialogInterface, _: Int ->
|
builder.setPositiveButton(R.string.continue_2) { dialog: DialogInterface, _: Int ->
|
||||||
textSecurePreferences.setHasSeenGIFMetaDataWarning()
|
textSecurePreferences.setHasSeenGIFMetaDataWarning()
|
||||||
AttachmentManager.selectGif(this, PICK_GIF)
|
AttachmentManager.selectGif(this, PICK_GIF)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
builder.setNegativeButton(
|
builder.setNegativeButton(R.string.cancel) { dialog: DialogInterface, _: Int ->
|
||||||
"Cancel"
|
dialog.dismiss()
|
||||||
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
|
}
|
||||||
builder.create().show()
|
builder.create().show()
|
||||||
} else {
|
} else {
|
||||||
AttachmentManager.selectGif(this, PICK_GIF)
|
AttachmentManager.selectGif(this, PICK_GIF)
|
||||||
|
@ -78,6 +78,10 @@ object ConversationMenuHelper {
|
|||||||
inflater.inflate(R.menu.menu_conversation_expiration_off, menu)
|
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)
|
// One-on-one chat menu (options that should only be present for one-on-one chats)
|
||||||
if (thread.isContactRecipient) {
|
if (thread.isContactRecipient) {
|
||||||
if (thread.isBlocked) {
|
if (thread.isBlocked) {
|
||||||
@ -154,6 +158,7 @@ object ConversationMenuHelper {
|
|||||||
R.id.menu_block -> { block(context, thread, deleteThread = false) }
|
R.id.menu_block -> { block(context, thread, deleteThread = false) }
|
||||||
R.id.menu_block_delete -> { blockAndDelete(context, thread) }
|
R.id.menu_block_delete -> { blockAndDelete(context, thread) }
|
||||||
R.id.menu_copy_session_id -> { copySessionID(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_edit_group -> { editClosedGroup(context, thread) }
|
||||||
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
|
R.id.menu_leave_group -> { leaveClosedGroup(context, thread) }
|
||||||
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
|
R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
|
||||||
@ -270,6 +275,12 @@ object ConversationMenuHelper {
|
|||||||
listener.copySessionID(thread.address.toString())
|
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) {
|
private fun editClosedGroup(context: Context, thread: Recipient) {
|
||||||
if (!thread.isClosedGroupRecipient) { return }
|
if (!thread.isClosedGroupRecipient) { return }
|
||||||
val intent = Intent(context, EditClosedGroupActivity::class.java)
|
val intent = Intent(context, EditClosedGroupActivity::class.java)
|
||||||
@ -344,6 +355,7 @@ object ConversationMenuHelper {
|
|||||||
fun block(deleteThread: Boolean = false)
|
fun block(deleteThread: Boolean = false)
|
||||||
fun unblock()
|
fun unblock()
|
||||||
fun copySessionID(sessionId: String)
|
fun copySessionID(sessionId: String)
|
||||||
|
fun copyOpenGroupUrl(thread: Recipient)
|
||||||
fun showExpiringMessagesDialog(thread: Recipient)
|
fun showExpiringMessagesDialog(thread: Recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,8 +57,12 @@ class LinkPreviewView : LinearLayout {
|
|||||||
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
val cornerRadii = MessageBubbleUtilities.calculateRadii(context, isStartOfMessageCluster, isEndOfMessageCluster, message.isOutgoing)
|
||||||
cornerMask.setTopLeftRadius(cornerRadii[0])
|
cornerMask.setTopLeftRadius(cornerRadii[0])
|
||||||
cornerMask.setTopRightRadius(cornerRadii[1])
|
cornerMask.setTopRightRadius(cornerRadii[1])
|
||||||
cornerMask.setBottomRightRadius(cornerRadii[2])
|
|
||||||
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
// Only round the bottom corners if there is no body text
|
||||||
|
if (message.body.isEmpty()) {
|
||||||
|
cornerMask.setBottomRightRadius(cornerRadii[2])
|
||||||
|
cornerMask.setBottomLeftRadius(cornerRadii[3])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispatchDraw(canvas: Canvas) {
|
override fun dispatchDraw(canvas: Canvas) {
|
||||||
|
@ -158,7 +158,9 @@ class VisibleMessageContentView : ConstraintLayout {
|
|||||||
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
|
message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> {
|
||||||
binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
binding.linkPreviewView.root.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster)
|
||||||
onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) }
|
onContentClick.add { event -> binding.linkPreviewView.root.calculateHit(event) }
|
||||||
// Body text view is inside the link preview for layout convenience
|
|
||||||
|
// When in a link preview ensure the bodyTextView can expand to the full width
|
||||||
|
binding.bodyTextView.maxWidth = binding.linkPreviewView.root.layoutParams.width
|
||||||
}
|
}
|
||||||
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
|
message is MmsMessageRecord && message.slideDeck.audioSlide != null -> {
|
||||||
hideBody = true
|
hideBody = true
|
||||||
|
@ -123,12 +123,13 @@ open class ThumbnailView: FrameLayout {
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
slide.thumbnailUri != null -> {
|
slide.thumbnailUri != null -> {
|
||||||
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, result))
|
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result))
|
||||||
}
|
}
|
||||||
slide.hasPlaceholder() -> {
|
slide.hasPlaceholder() -> {
|
||||||
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, result))
|
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
binding.thumbnailLoadIndicator.isVisible = false
|
||||||
glide.clear(binding.thumbnailImage)
|
glide.clear(binding.thumbnailImage)
|
||||||
result.set(false)
|
result.set(false)
|
||||||
}
|
}
|
||||||
@ -190,7 +191,7 @@ open class ThumbnailView: FrameLayout {
|
|||||||
request.transforms(CenterCrop())
|
request.transforms(CenterCrop())
|
||||||
}
|
}
|
||||||
|
|
||||||
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, future))
|
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, future))
|
||||||
|
|
||||||
return future
|
return future
|
||||||
}
|
}
|
||||||
|
@ -318,6 +318,25 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
|
|||||||
notifyConversationListListeners();
|
notifyConversationListListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeProfilePicture(String groupID) {
|
||||||
|
databaseHelper.getWritableDatabase()
|
||||||
|
.execSQL("UPDATE " + TABLE_NAME +
|
||||||
|
" SET " + AVATAR + " = NULL, " +
|
||||||
|
AVATAR_ID + " = NULL, " +
|
||||||
|
AVATAR_KEY + " = NULL, " +
|
||||||
|
AVATAR_CONTENT_TYPE + " = NULL, " +
|
||||||
|
AVATAR_RELAY + " = NULL, " +
|
||||||
|
AVATAR_DIGEST + " = NULL, " +
|
||||||
|
AVATAR_URL + " = NULL" +
|
||||||
|
" WHERE " +
|
||||||
|
GROUP_ID + " = ?",
|
||||||
|
new String[] {groupID});
|
||||||
|
|
||||||
|
Recipient.applyCached(Address.fromSerialized(groupID), recipient -> recipient.setGroupAvatarId(null));
|
||||||
|
notifyConversationListListeners();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasDownloadedProfilePicture(String groupId) {
|
public boolean hasDownloadedProfilePicture(String groupId) {
|
||||||
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?",
|
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[]{AVATAR}, GROUP_ID + " = ?",
|
||||||
new String[] {groupId},
|
new String[] {groupId},
|
||||||
|
@ -83,11 +83,11 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? {
|
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): GroupAvatarDownloadJob? {
|
||||||
val database = databaseHelper.readableDatabase
|
val database = databaseHelper.readableDatabase
|
||||||
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(GroupAvatarDownloadJob.KEY)) {
|
return database.getAll(sessionJobTable, "$jobType = ?", arrayOf(GroupAvatarDownloadJob.KEY)) {
|
||||||
jobFromCursor(it) as GroupAvatarDownloadJob?
|
jobFromCursor(it) as GroupAvatarDownloadJob?
|
||||||
}.filterNotNull().find { it.server == server && it.room == room }
|
}.filterNotNull().find { it.server == server && it.room == room && (imageId == null || it.imageId == imageId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelPendingMessageSendJobs(threadID: Long) {
|
fun cancelPendingMessageSendJobs(threadID: Long) {
|
||||||
|
@ -249,8 +249,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
|
|||||||
return DatabaseComponent.get(context).sessionJobDatabase().getMessageReceiveJob(messageReceiveJobID)
|
return DatabaseComponent.get(context).sessionJobDatabase().getMessageReceiveJob(messageReceiveJobID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGroupAvatarDownloadJob(server: String, room: String): GroupAvatarDownloadJob? {
|
override fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): GroupAvatarDownloadJob? {
|
||||||
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room)
|
return DatabaseComponent.get(context).sessionJobDatabase().getGroupAvatarDownloadJob(server, room, imageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getConfigSyncJob(destination: Destination): Job? {
|
override fun getConfigSyncJob(destination: Destination): Job? {
|
||||||
@ -432,6 +432,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
|
|||||||
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
|
DatabaseComponent.get(context).groupDatabase().updateProfilePicture(groupID, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun removeProfilePicture(groupID: String) {
|
||||||
|
DatabaseComponent.get(context).groupDatabase().removeProfilePicture(groupID)
|
||||||
|
}
|
||||||
|
|
||||||
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
|
override fun hasDownloadedProfilePicture(groupID: String): Boolean {
|
||||||
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
|
return DatabaseComponent.get(context).groupDatabase().hasDownloadedProfilePicture(groupID)
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,6 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
|
||||||
import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
|
|
||||||
import org.thoughtcrime.securesms.mms.Slide;
|
import org.thoughtcrime.securesms.mms.Slide;
|
||||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||||
@ -802,77 +801,6 @@ public class ThreadDatabase extends Database {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public List<ThreadRecord> 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<ThreadRecord> 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<ThreadRecord> 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<ThreadRecord> 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<ThreadRecord> 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<ThreadRecord> 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) {
|
public void migrateEncodedGroup(long threadId, @NotNull String newEncodedGroupId) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(ADDRESS, newEncodedGroupId);
|
contentValues.put(ADDRESS, newEncodedGroupId);
|
||||||
|
@ -100,25 +100,40 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private final DatabaseSecret databaseSecret;
|
private final DatabaseSecret databaseSecret;
|
||||||
|
|
||||||
public SQLCipherOpenHelper(@NonNull Context context, @NonNull 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() {
|
super(
|
||||||
@Override
|
context,
|
||||||
public void preKey(SQLiteConnection connection) {
|
DATABASE_NAME,
|
||||||
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
databaseSecret.asString(),
|
||||||
}
|
null,
|
||||||
|
DATABASE_VERSION,
|
||||||
@Override
|
MIN_DATABASE_VERSION,
|
||||||
public void postKey(SQLiteConnection connection) {
|
null,
|
||||||
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
new SQLiteDatabaseHook() {
|
||||||
|
@Override
|
||||||
// if not vacuumed in a while, perform that operation
|
public void preKey(SQLiteConnection connection) {
|
||||||
long currentTime = System.currentTimeMillis();
|
SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
|
||||||
// 7 days
|
|
||||||
if (currentTime - TextSecurePreferences.getLastVacuumTime(context) > 604_800_000) {
|
|
||||||
connection.execute("VACUUM;", null, null);
|
|
||||||
TextSecurePreferences.setLastVacuumNow(context);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, 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.context = context.getApplicationContext();
|
||||||
this.databaseSecret = databaseSecret;
|
this.databaseSecret = databaseSecret;
|
||||||
@ -153,11 +168,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
// If the old SQLCipher3 database file doesn't exist then no need to do anything
|
// If the old SQLCipher3 database file doesn't exist then no need to do anything
|
||||||
if (!oldDbFile.exists()) { return; }
|
if (!oldDbFile.exists()) { return; }
|
||||||
|
|
||||||
try {
|
// Define the location for the new database
|
||||||
// Define the location for the new database
|
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
|
||||||
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
|
File newDbFile = new File(newDbPath);
|
||||||
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
|
// 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
|
// invalid state we should delete it and try to migrate again
|
||||||
if (newDbFile.exists()) {
|
if (newDbFile.exists()) {
|
||||||
@ -165,10 +180,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
// assume the user hasn't downgraded for some reason and made changes to the old database and
|
// 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)
|
// can remove the old database file (it won't be used anymore)
|
||||||
if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
|
if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
|
||||||
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
|
try {
|
||||||
// //noinspection ResultOfMethodCallIgnored
|
SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true);
|
||||||
// oldDbFile.delete();
|
int version = newDb.getVersion();
|
||||||
return;
|
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
|
// If the old database does have newer changes then the new database could have stale/invalid
|
||||||
@ -210,6 +239,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", 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
|
// Notify the user of the issue so they know they can downgrade until the issue is fixed
|
||||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||||
String channelId = context.getString(R.string.NotificationChannel_failures);
|
String channelId = context.getString(R.string.NotificationChannel_failures);
|
||||||
@ -572,6 +606,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) {
|
public void markCurrent(SQLiteDatabase db) {
|
||||||
db.setVersion(DATABASE_VERSION);
|
db.setVersion(DATABASE_VERSION);
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,7 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
private final long expiresIn;
|
private final long expiresIn;
|
||||||
private final long lastSeen;
|
private final long lastSeen;
|
||||||
private final boolean pinned;
|
private final boolean pinned;
|
||||||
|
private final int initialRecipientHash;
|
||||||
|
|
||||||
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
public ThreadRecord(@NonNull String body, @Nullable Uri snippetUri,
|
||||||
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
@NonNull Recipient recipient, long date, long count, int unreadCount,
|
||||||
@ -68,6 +69,7 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
this.expiresIn = expiresIn;
|
this.expiresIn = expiresIn;
|
||||||
this.lastSeen = lastSeen;
|
this.lastSeen = lastSeen;
|
||||||
this.pinned = pinned;
|
this.pinned = pinned;
|
||||||
|
this.initialRecipientHash = recipient.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable Uri getSnippetUri() {
|
public @Nullable Uri getSnippetUri() {
|
||||||
@ -176,4 +178,8 @@ public class ThreadRecord extends DisplayRecord {
|
|||||||
public boolean isPinned() {
|
public boolean isPinned() {
|
||||||
return pinned;
|
return pinned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getInitialRecipientHash() {
|
||||||
|
return initialRecipientHash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<ThreadRecord>, new: List<ThreadRecord>): List<OpenGroupMapping> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -20,6 +20,7 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
|
|||||||
lateinit var thread: ThreadRecord
|
lateinit var thread: ThreadRecord
|
||||||
|
|
||||||
var onViewDetailsTapped: (() -> Unit?)? = null
|
var onViewDetailsTapped: (() -> Unit?)? = null
|
||||||
|
var onCopyConversationId: (() -> Unit?)? = null
|
||||||
var onPinTapped: (() -> Unit)? = null
|
var onPinTapped: (() -> Unit)? = null
|
||||||
var onUnpinTapped: (() -> Unit)? = null
|
var onUnpinTapped: (() -> Unit)? = null
|
||||||
var onBlockTapped: (() -> Unit)? = null
|
var onBlockTapped: (() -> Unit)? = null
|
||||||
@ -37,6 +38,8 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
|
|||||||
override fun onClick(v: View?) {
|
override fun onClick(v: View?) {
|
||||||
when (v) {
|
when (v) {
|
||||||
binding.detailsTextView -> onViewDetailsTapped?.invoke()
|
binding.detailsTextView -> onViewDetailsTapped?.invoke()
|
||||||
|
binding.copyConversationId -> onCopyConversationId?.invoke()
|
||||||
|
binding.copyCommunityUrl -> onCopyConversationId?.invoke()
|
||||||
binding.pinTextView -> onPinTapped?.invoke()
|
binding.pinTextView -> onPinTapped?.invoke()
|
||||||
binding.unpinTextView -> onUnpinTapped?.invoke()
|
binding.unpinTextView -> onUnpinTapped?.invoke()
|
||||||
binding.blockTextView -> onBlockTapped?.invoke()
|
binding.blockTextView -> onBlockTapped?.invoke()
|
||||||
@ -63,6 +66,10 @@ class ConversationOptionsBottomSheet(private val parentContext: Context) : Botto
|
|||||||
} else {
|
} else {
|
||||||
binding.detailsTextView.visibility = View.GONE
|
binding.detailsTextView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
binding.copyConversationId.visibility = if (!recipient.isGroupRecipient && !recipient.isLocalNumber) View.VISIBLE else View.GONE
|
||||||
|
binding.copyConversationId.setOnClickListener(this)
|
||||||
|
binding.copyCommunityUrl.visibility = if (recipient.isOpenGroupRecipient) View.VISIBLE else View.GONE
|
||||||
|
binding.copyCommunityUrl.setOnClickListener(this)
|
||||||
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
binding.unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
|
||||||
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
binding.muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
|
||||||
binding.unMuteNotificationsTextView.setOnClickListener(this)
|
binding.unMuteNotificationsTextView.setOnClickListener(this)
|
||||||
|
@ -4,6 +4,8 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@ -446,6 +448,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
userDetailsBottomSheet.arguments = bundle
|
userDetailsBottomSheet.arguments = bundle
|
||||||
userDetailsBottomSheet.show(supportFragmentManager, userDetailsBottomSheet.tag)
|
userDetailsBottomSheet.show(supportFragmentManager, userDetailsBottomSheet.tag)
|
||||||
}
|
}
|
||||||
|
bottomSheet.onCopyConversationId = onCopyConversationId@{
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) {
|
||||||
|
val clip = ClipData.newPlainText("Session ID", thread.recipient.address.toString())
|
||||||
|
val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
manager.setPrimaryClip(clip)
|
||||||
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
else if (thread.recipient.isOpenGroupRecipient) {
|
||||||
|
val threadId = threadDb.getThreadIdIfExistsFor(thread.recipient) ?: return@onCopyConversationId Unit
|
||||||
|
val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
bottomSheet.onBlockTapped = {
|
bottomSheet.onBlockTapped = {
|
||||||
bottomSheet.dismiss()
|
bottomSheet.dismiss()
|
||||||
if (!thread.recipient.isBlocked) {
|
if (!thread.recipient.isBlocked) {
|
||||||
|
@ -28,8 +28,11 @@ class HomeDiffUtil(
|
|||||||
if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) }
|
if (isSameItem) { isSameItem = (oldItem.unreadCount == newItem.unreadCount) }
|
||||||
if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) }
|
if (isSameItem) { isSameItem = (oldItem.isPinned == newItem.isPinned) }
|
||||||
|
|
||||||
// Note: For some reason the 'hashCode' value can change after initialisation so we can't cache it
|
// The recipient is passed as a reference and changes to recipients update the reference so we
|
||||||
if (isSameItem) { isSameItem = (oldItem.recipient.hashCode() == newItem.recipient.hashCode()) }
|
// need to cache the hashCode for the recipient and use that for diffing - unfortunately
|
||||||
|
// recipient data is also loaded asyncronously which means every thread will refresh at least
|
||||||
|
// once when the initial recipient data is loaded
|
||||||
|
if (isSameItem) { isSameItem = (oldItem.initialRecipientHash == newItem.initialRecipientHash) }
|
||||||
|
|
||||||
// Note: Two instances of 'SpannableString' may not equate even though their content matches
|
// Note: Two instances of 'SpannableString' may not equate even though their content matches
|
||||||
if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) }
|
if (isSameItem) { isSameItem = (oldItem.getDisplayBody(context).toString() == newItem.getDisplayBody(context).toString()) }
|
||||||
|
@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getImageMaxSize(Context context) {
|
public int getImageMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
return FileServerApi.maxFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getGifMaxSize(Context context) {
|
public int getGifMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
return FileServerApi.maxFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getVideoMaxSize(Context context) {
|
public int getVideoMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
return FileServerApi.maxFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAudioMaxSize(Context context) {
|
public int getAudioMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
return FileServerApi.maxFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDocumentMaxSize(Context context) {
|
public int getDocumentMaxSize(Context context) {
|
||||||
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
|
return FileServerApi.maxFileSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
// FIXME: Using a job here seems like a bad idea...
|
// FIXME: Using a job here seems like a bad idea...
|
||||||
MessageReceiveParameters(envelope.toByteArray(), serverHash, null)
|
MessageReceiveParameters(envelope.toByteArray(), serverHash, null)
|
||||||
}
|
}
|
||||||
BatchMessageReceiveJob(params).executeAsync()
|
BatchMessageReceiveJob(params).executeAsync("background")
|
||||||
}
|
}
|
||||||
promises.add(dmsPromise)
|
promises.add(dmsPromise)
|
||||||
|
|
||||||
|
@ -16,6 +16,22 @@
|
|||||||
android:drawableTint="?attr/colorControlNormal"
|
android:drawableTint="?attr/colorControlNormal"
|
||||||
android:text="@string/details" />
|
android:text="@string/details" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/copyConversationId"
|
||||||
|
style="@style/BottomSheetActionItem"
|
||||||
|
android:drawableStart="@drawable/ic_content_copy_white_24dp"
|
||||||
|
android:drawableTint="?attr/colorControlNormal"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/activity_conversation_menu_copy_session_id" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/copyCommunityUrl"
|
||||||
|
style="@style/BottomSheetActionItem"
|
||||||
|
android:drawableStart="@drawable/ic_content_copy_white_24dp"
|
||||||
|
android:drawableTint="?attr/colorControlNormal"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:text="@string/ConversationActivity_copy_open_group_url" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pinTextView"
|
android:id="@+id/pinTextView"
|
||||||
style="@style/BottomSheetActionItem"
|
style="@style/BottomSheetActionItem"
|
||||||
|
@ -80,7 +80,7 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:id="@+id/linkPreviewView"
|
android:id="@+id/linkPreviewView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="300dp"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
<menu
|
<menu
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:title="@string/ConversationActivity_copy_open_group_url"
|
||||||
|
android:id="@+id/menu_copy_open_group_url" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:title="@string/ConversationActivity_invite_to_open_group"
|
android:title="@string/ConversationActivity_invite_to_open_group"
|
||||||
android:id="@+id/menu_invite_to_open_group" />
|
android:id="@+id/menu_invite_to_open_group" />
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Session</string>
|
|
||||||
<string name="yes">Oui</string>
|
<string name="yes">Oui</string>
|
||||||
<string name="no">Non</string>
|
<string name="no">Non</string>
|
||||||
<string name="delete">Supprimer</string>
|
<string name="delete">Supprimer</string>
|
||||||
<string name="ban">Bannir</string>
|
<string name="ban">Bannir</string>
|
||||||
|
<string name="please_wait">Veuillez patienter…</string>
|
||||||
<string name="save">Enregistrer</string>
|
<string name="save">Enregistrer</string>
|
||||||
<string name="note_to_self">Note à mon intention</string>
|
<string name="note_to_self">Note à mon intention</string>
|
||||||
<string name="version_s">Version %s</string>
|
<string name="version_s">Version %s</string>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="ApplicationPreferencesActivity_delete_all_old_messages_now">Supprimer tous les anciens messages maintenant ?</string>
|
<string name="ApplicationPreferencesActivity_delete_all_old_messages_now">Supprimer tous les anciens messages maintenant ?</string>
|
||||||
<plurals name="ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages">
|
<plurals name="ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages">
|
||||||
<item quantity="one">Cela va immédiatement réduire toutes les conversations pour qu’il ne reste que le message le plus récent.</item>
|
<item quantity="one">Cela réduira immédiatement toutes les conversations au message le plus récent.</item>
|
||||||
<item quantity="other">Cela réduira immédiatement toutes les conversations aux %d messages les plus récents.</item>
|
<item quantity="other">Cela réduira immédiatement toutes les conversations aux %d messages les plus récents.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="ApplicationPreferencesActivity_delete">Supprimer</string>
|
<string name="ApplicationPreferencesActivity_delete">Supprimer</string>
|
||||||
@ -63,6 +63,7 @@
|
|||||||
<string name="ConversationActivity_muted_until_date">Son désactivé jusqu\'à %1$s</string>
|
<string name="ConversationActivity_muted_until_date">Son désactivé jusqu\'à %1$s</string>
|
||||||
<string name="ConversationActivity_muted_forever">En sourdine</string>
|
<string name="ConversationActivity_muted_forever">En sourdine</string>
|
||||||
<string name="ConversationActivity_member_count">%1$d membres</string>
|
<string name="ConversationActivity_member_count">%1$d membres</string>
|
||||||
|
<string name="ConversationActivity_active_member_count">%1$d membres actifs</string>
|
||||||
<string name="ConversationActivity_open_group_guidelines">Règles de la communauté</string>
|
<string name="ConversationActivity_open_group_guidelines">Règles de la communauté</string>
|
||||||
<string name="ConversationActivity_invalid_recipient">Le destinataire est invalide !</string>
|
<string name="ConversationActivity_invalid_recipient">Le destinataire est invalide !</string>
|
||||||
<string name="ConversationActivity_added_to_home_screen">Ajouté à l’écran d’accueil</string>
|
<string name="ConversationActivity_added_to_home_screen">Ajouté à l’écran d’accueil</string>
|
||||||
@ -82,7 +83,9 @@
|
|||||||
<string name="ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage">Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos.</string>
|
<string name="ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage">Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos.</string>
|
||||||
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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.</string>
|
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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.</string>
|
||||||
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos</string>
|
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos</string>
|
||||||
<string name="ConversationActivity_search_position">%1$d de %2$d</string>
|
<string name="ConversationActivity_search_position">%1$d sur %2$d</string>
|
||||||
|
<string name="ConversationActivity_call_title">Autorisations d\'appel requises</string>
|
||||||
|
<string name="ConversationActivity_call_prompt">Vous pouvez activer la permission \"Appels vocaux et vidéo\" dans les paramètres de confidentialité.</string>
|
||||||
<!-- ConversationFragment -->
|
<!-- ConversationFragment -->
|
||||||
<plurals name="ConversationFragment_delete_selected_messages">
|
<plurals name="ConversationFragment_delete_selected_messages">
|
||||||
<item quantity="one">Supprimer le message sélectionné ?</item>
|
<item quantity="one">Supprimer le message sélectionné ?</item>
|
||||||
@ -99,13 +102,17 @@
|
|||||||
<item quantity="other">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 ?</item>
|
<item quantity="other">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 ?</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
|
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
|
||||||
<item quantity="one">Erreur d’enregistrement de la pièce jointe dans la mémoire !</item>
|
<item quantity="one">Erreur lors de l’enregistrement de la pièce jointe dans la mémoire !</item>
|
||||||
<item quantity="other">Erreur d’enregistrement des pièces jointes dans la mémoire !</item>
|
<item quantity="other">Erreur d’enregistrement des pièces jointes dans la mémoire !</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="ConversationFragment_saving_n_attachments">
|
<plurals name="ConversationFragment_saving_n_attachments">
|
||||||
<item quantity="one">Enregistrement de la pièce jointe</item>
|
<item quantity="one">Enregistrement de la pièce jointe</item>
|
||||||
<item quantity="other">Enregistrement de %1$d pièces jointes</item>
|
<item quantity="other">Enregistrement de %1$d pièces jointes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="ConversationFragment_saving_n_attachments_to_sd_card">
|
||||||
|
<item quantity="one">Enregistrement de la pièce jointe dans la mémoire…</item>
|
||||||
|
<item quantity="other">Enregistrement de %1$d pièces jointes dans la mémoire…</item>
|
||||||
|
</plurals>
|
||||||
<!-- CreateProfileActivity -->
|
<!-- CreateProfileActivity -->
|
||||||
<string name="CreateProfileActivity_profile_photo">Photo de profil</string>
|
<string name="CreateProfileActivity_profile_photo">Photo de profil</string>
|
||||||
<!-- CustomDefaultPreference -->
|
<!-- CustomDefaultPreference -->
|
||||||
@ -156,7 +163,7 @@
|
|||||||
<!-- MediaPickerActivity -->
|
<!-- MediaPickerActivity -->
|
||||||
<string name="MediaPickerActivity_send_to">Envoyer à %s</string>
|
<string name="MediaPickerActivity_send_to">Envoyer à %s</string>
|
||||||
<!-- MediaSendActivity -->
|
<!-- MediaSendActivity -->
|
||||||
<string name="MediaSendActivity_add_a_caption">Ajouter un légende…</string>
|
<string name="MediaSendActivity_add_a_caption">Ajouter une légende...</string>
|
||||||
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">Un élément a été supprimé, car il dépassait la taille limite</string>
|
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">Un élément a été supprimé, car il dépassait la taille limite</string>
|
||||||
<string name="MediaSendActivity_camera_unavailable">L’appareil photo n’est pas disponible</string>
|
<string name="MediaSendActivity_camera_unavailable">L’appareil photo n’est pas disponible</string>
|
||||||
<string name="MediaSendActivity_message_to_s">Message à %s</string>
|
<string name="MediaSendActivity_message_to_s">Message à %s</string>
|
||||||
@ -191,6 +198,7 @@
|
|||||||
<string name="Slide_video">Vidéo</string>
|
<string name="Slide_video">Vidéo</string>
|
||||||
<!-- SmsMessageRecord -->
|
<!-- SmsMessageRecord -->
|
||||||
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Vous avez reçu un message d’échange de clés corrompu !</string>
|
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Vous avez reçu un message d’échange de clés corrompu !</string>
|
||||||
|
<string name="SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version"> Le message d\'échange de clé reçu est pour une version du protocole invalide. </string>
|
||||||
<string name="SmsMessageRecord_received_message_with_new_safety_number_tap_to_process">Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher.</string>
|
<string name="SmsMessageRecord_received_message_with_new_safety_number_tap_to_process">Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher.</string>
|
||||||
<string name="SmsMessageRecord_secure_session_reset">Vous avez réinitialisé la session sécurisée.</string>
|
<string name="SmsMessageRecord_secure_session_reset">Vous avez réinitialisé la session sécurisée.</string>
|
||||||
<string name="SmsMessageRecord_secure_session_reset_s">%s a réinitialisé la session sécurisée.</string>
|
<string name="SmsMessageRecord_secure_session_reset_s">%s a réinitialisé la session sécurisée.</string>
|
||||||
@ -201,7 +209,7 @@
|
|||||||
<string name="ThreadRecord_secure_session_reset">La session sécurisée a été réinitialisée.</string>
|
<string name="ThreadRecord_secure_session_reset">La session sécurisée a été réinitialisée.</string>
|
||||||
<string name="ThreadRecord_draft">Brouillon :</string>
|
<string name="ThreadRecord_draft">Brouillon :</string>
|
||||||
<string name="ThreadRecord_called">Vous avez appelé</string>
|
<string name="ThreadRecord_called">Vous avez appelé</string>
|
||||||
<string name="ThreadRecord_called_you">Vous a appelé</string>
|
<string name="ThreadRecord_called_you">Vous a appelé·e</string>
|
||||||
<string name="ThreadRecord_missed_call">Appel manqué</string>
|
<string name="ThreadRecord_missed_call">Appel manqué</string>
|
||||||
<string name="ThreadRecord_media_message">Message multimédia</string>
|
<string name="ThreadRecord_media_message">Message multimédia</string>
|
||||||
<string name="ThreadRecord_s_is_on_signal">%s est sur Session !</string>
|
<string name="ThreadRecord_s_is_on_signal">%s est sur Session !</string>
|
||||||
@ -304,7 +312,7 @@
|
|||||||
<string name="conversation_activity__send">Envoyer</string>
|
<string name="conversation_activity__send">Envoyer</string>
|
||||||
<string name="conversation_activity__compose_description">Rédaction d’un message</string>
|
<string name="conversation_activity__compose_description">Rédaction d’un message</string>
|
||||||
<string name="conversation_activity__emoji_toggle_description">Afficher, masquer le clavier des émojis</string>
|
<string name="conversation_activity__emoji_toggle_description">Afficher, masquer le clavier des émojis</string>
|
||||||
<string name="conversation_activity__attachment_thumbnail">Imagette de pièces jointes</string>
|
<string name="conversation_activity__attachment_thumbnail">Vignette de pièce jointe</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution</string>
|
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Enregistrer et envoyer une pièce jointe audio</string>
|
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Enregistrer et envoyer une pièce jointe audio</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Verrouiller l’enregistrement de pièces jointes audio</string>
|
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Verrouiller l’enregistrement de pièces jointes audio</string>
|
||||||
@ -378,9 +386,9 @@
|
|||||||
<string name="arrays__settings_default">Valeur par défaut</string>
|
<string name="arrays__settings_default">Valeur par défaut</string>
|
||||||
<string name="arrays__enabled">Activé</string>
|
<string name="arrays__enabled">Activé</string>
|
||||||
<string name="arrays__disabled">Désactivé</string>
|
<string name="arrays__disabled">Désactivé</string>
|
||||||
<string name="arrays__name_and_message">Nom et message</string>
|
<string name="arrays__name_and_message">Nom et Contenu</string>
|
||||||
<string name="arrays__name_only">Nom seulement</string>
|
<string name="arrays__name_only">Nom uniquement</string>
|
||||||
<string name="arrays__no_name_or_message">Aucun nom ni message</string>
|
<string name="arrays__no_name_or_message">Aucun nom ni contenu</string>
|
||||||
<string name="arrays__images">Images</string>
|
<string name="arrays__images">Images</string>
|
||||||
<string name="arrays__audio">Son</string>
|
<string name="arrays__audio">Son</string>
|
||||||
<string name="arrays__video">Vidéo</string>
|
<string name="arrays__video">Vidéo</string>
|
||||||
@ -398,9 +406,14 @@
|
|||||||
<item quantity="other">%d heures</item>
|
<item quantity="other">%d heures</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<!-- preferences.xml -->
|
<!-- preferences.xml -->
|
||||||
<string name="preferences__pref_enter_sends_title">La touche Entrée envoie</string>
|
<string name="preferences__pref_enter_sends_title">Envoyer avec bouton Entrée</string>
|
||||||
<string name="preferences__send_link_previews">Envoyer des aperçus de liens</string>
|
<string name="preferences__pref_enter_sends_summary">Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne.</string>
|
||||||
<string name="preferences__previews_are_supported_for">Les aperçus sont pris en charge pour les liens Imgur, Instagram, Pinterest, Reddit et YouTube</string>
|
<string name="preferences__send_link_previews">Envoyer les aperçus des liens</string>
|
||||||
|
<string name="preferences__link_previews">Aperçus des liens</string>
|
||||||
|
<string name="preferences__link_previews_summary">Générer les aperçus des liens pour les URLs supportés.</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_category">Messages audio</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_title">Lire automatiquement les messages audios</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_summary">Lire automatiquement les messages audio consécutifs.</string>
|
||||||
<string name="preferences__screen_security">Sécurité de l’écran</string>
|
<string name="preferences__screen_security">Sécurité de l’écran</string>
|
||||||
<string name="preferences__disable_screen_security_to_allow_screen_shots">Bloquer les captures d’écran dans la liste des récents et dans l’appli</string>
|
<string name="preferences__disable_screen_security_to_allow_screen_shots">Bloquer les captures d’écran dans la liste des récents et dans l’appli</string>
|
||||||
<string name="preferences__notifications">Notifications</string>
|
<string name="preferences__notifications">Notifications</string>
|
||||||
@ -408,6 +421,7 @@
|
|||||||
<string name="preferences__led_color_unknown">Inconnue</string>
|
<string name="preferences__led_color_unknown">Inconnue</string>
|
||||||
<string name="preferences__pref_led_blink_title">Rythme de clignotement de la DEL</string>
|
<string name="preferences__pref_led_blink_title">Rythme de clignotement de la DEL</string>
|
||||||
<string name="preferences__sound">Son</string>
|
<string name="preferences__sound">Son</string>
|
||||||
|
<string name="preferences__in_app_sounds">Son à l\'ouverture de l\'application</string>
|
||||||
<string name="preferences__silent">Silencieux</string>
|
<string name="preferences__silent">Silencieux</string>
|
||||||
<string name="preferences__repeat_alerts">Répéter les alertes</string>
|
<string name="preferences__repeat_alerts">Répéter les alertes</string>
|
||||||
<string name="preferences__never">Jamais</string>
|
<string name="preferences__never">Jamais</string>
|
||||||
@ -436,18 +450,27 @@
|
|||||||
<string name="preferences__default">Valeur par défaut</string>
|
<string name="preferences__default">Valeur par défaut</string>
|
||||||
<string name="preferences__incognito_keyboard">Clavier incognito</string>
|
<string name="preferences__incognito_keyboard">Clavier incognito</string>
|
||||||
<string name="preferences__read_receipts">Accusés de lecture</string>
|
<string name="preferences__read_receipts">Accusés de lecture</string>
|
||||||
|
<string name="preferences__read_receipts_summary">Envoyer des accusés de lecture dans les conversations individuelles.</string>
|
||||||
<string name="preferences__typing_indicators">Indicateurs de saisie</string>
|
<string name="preferences__typing_indicators">Indicateurs de saisie</string>
|
||||||
<string name="preferences__if_typing_indicators_are_disabled_you_wont_be_able_to_see_typing_indicators">Si les indicateurs de saisie sont désactivés, vous ne serez pas en mesure de voir les indicateurs de saisie des autres.</string>
|
<string name="preferences__typing_indicators_summary">Voir et envoyer les indicateurs de saisie dans les conversations un à un.</string>
|
||||||
<string name="preferences__request_keyboard_to_disable_personalized_learning">Demander au clavier de désactiver l’apprentissage personnalisé</string>
|
<string name="preferences__request_keyboard_to_disable_personalized_learning">Demander au clavier de désactiver l’apprentissage personnalisé</string>
|
||||||
<string name="preferences__light_theme">Clair</string>
|
<string name="preferences__light_theme">Clair</string>
|
||||||
<string name="preferences__dark_theme">Sombre</string>
|
<string name="preferences__dark_theme">Sombre</string>
|
||||||
<string name="preferences_chats__message_trimming">Élagage des messages</string>
|
<string name="preferences_chats__message_trimming">Élagage des messages</string>
|
||||||
|
<string name="preferences_chats__message_trimming_title">Raccourcir les communautés</string>
|
||||||
|
<string name="preferences_chats__message_trimming_summary">Supprimer des messages de plus de 6 mois dans des communautés qui ont plus de 2 000 messages.</string>
|
||||||
<string name="preferences_advanced__use_system_emoji">Utiliser les émojis du système</string>
|
<string name="preferences_advanced__use_system_emoji">Utiliser les émojis du système</string>
|
||||||
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Désactiver la prise en charge des émojis intégrés à Session</string>
|
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Désactiver la prise en charge des émojis intégrés à Session</string>
|
||||||
|
<string name="preferences_app_protection__screen_security">Sécurité d\'écran</string>
|
||||||
<string name="preferences_chats__chats">Conversations</string>
|
<string name="preferences_chats__chats">Conversations</string>
|
||||||
<string name="preferences_notifications__messages">Messages</string>
|
<string name="preferences_notifications__messages">Messages</string>
|
||||||
<string name="preferences_notifications__in_chat_sounds">Sons des conversations</string>
|
<string name="preferences_notifications__in_chat_sounds">Sons des conversations</string>
|
||||||
|
<string name="preferences_notifications__content">Contenu de la notification</string>
|
||||||
|
<string name="preferences_notifications__content_message">Afficher :</string>
|
||||||
|
<string name="preferences_notifications__summary">Informations affichées dans les notifications.</string>
|
||||||
<string name="preferences_notifications__priority">Priorité</string>
|
<string name="preferences_notifications__priority">Priorité</string>
|
||||||
|
<string name="preferences_app_protection__screenshot_notifications">Notifications de capture d\'écran</string>
|
||||||
|
<string name="preferences_app_protected__screenshot_notifications_summary">Recevoir une notification lorsqu\'un contact prend une capture d\'écran d\'une conversation individuelle.</string>
|
||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
<!-- menus -->
|
<!-- menus -->
|
||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
@ -460,7 +483,10 @@
|
|||||||
<string name="conversation_context__menu_ban_user">Bannir l\'utilisateur</string>
|
<string name="conversation_context__menu_ban_user">Bannir l\'utilisateur</string>
|
||||||
<string name="conversation_context__menu_ban_and_delete_all">Bannir et supprimer tout</string>
|
<string name="conversation_context__menu_ban_and_delete_all">Bannir et supprimer tout</string>
|
||||||
<string name="conversation_context__menu_resend_message">Renvoyer le message</string>
|
<string name="conversation_context__menu_resend_message">Renvoyer le message</string>
|
||||||
|
<string name="conversation_context__menu_reply">Répondre</string>
|
||||||
<string name="conversation_context__menu_reply_to_message">Répondre au message</string>
|
<string name="conversation_context__menu_reply_to_message">Répondre au message</string>
|
||||||
|
<string name="conversation_context__menu_call">Appeler</string>
|
||||||
|
<string name="conversation_context__menu_select">Sélectionner</string>
|
||||||
<!-- conversation_context_image -->
|
<!-- conversation_context_image -->
|
||||||
<string name="conversation_context_image__save_attachment">Enregistrer la pièce jointe</string>
|
<string name="conversation_context_image__save_attachment">Enregistrer la pièce jointe</string>
|
||||||
<!-- conversation_expiring_off -->
|
<!-- conversation_expiring_off -->
|
||||||
@ -513,8 +539,8 @@
|
|||||||
<string name="LocalBackupJob_creating_backup">Création de la sauvegarde…</string>
|
<string name="LocalBackupJob_creating_backup">Création de la sauvegarde…</string>
|
||||||
<string name="ProgressPreference_d_messages_so_far">%d messages pour l’instant</string>
|
<string name="ProgressPreference_d_messages_so_far">%d messages pour l’instant</string>
|
||||||
<string name="BackupUtil_never">Jamais</string>
|
<string name="BackupUtil_never">Jamais</string>
|
||||||
<string name="preferences_app_protection__screen_lock">Verrouillage de l’écran</string>
|
<string name="preferences_app_protection__screen_lock">Verrouiller Session</string>
|
||||||
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Verrouiller l’accès à Session avec le verrouillage de l’écran d’Android ou une empreinte</string>
|
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Nécessite une empreinte digitale, un code PIN, un schéma ou un mot de passe pour déverrouiller Session.</string>
|
||||||
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Délai d’inactivité avant verrouillage de l’écran</string>
|
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Délai d’inactivité avant verrouillage de l’écran</string>
|
||||||
<string name="AppProtectionPreferenceFragment_none">Aucune</string>
|
<string name="AppProtectionPreferenceFragment_none">Aucune</string>
|
||||||
<!-- Conversation activity -->
|
<!-- Conversation activity -->
|
||||||
@ -522,6 +548,7 @@
|
|||||||
<!-- Session -->
|
<!-- Session -->
|
||||||
<string name="continue_2">Continuer</string>
|
<string name="continue_2">Continuer</string>
|
||||||
<string name="copy">Copier</string>
|
<string name="copy">Copier</string>
|
||||||
|
<string name="close">Fermer</string>
|
||||||
<string name="invalid_url">URL non valide</string>
|
<string name="invalid_url">URL non valide</string>
|
||||||
<string name="copied_to_clipboard">Copié dans le presse-papier</string>
|
<string name="copied_to_clipboard">Copié dans le presse-papier</string>
|
||||||
<string name="next">Suivant</string>
|
<string name="next">Suivant</string>
|
||||||
@ -573,12 +600,12 @@
|
|||||||
<string name="activity_path_resolving_progress">Contact en cours…</string>
|
<string name="activity_path_resolving_progress">Contact en cours…</string>
|
||||||
<string name="activity_create_private_chat_title">Nouvelle Session</string>
|
<string name="activity_create_private_chat_title">Nouvelle Session</string>
|
||||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Saisir un Session ID</string>
|
<string name="activity_create_private_chat_enter_session_id_tab_title">Saisir un Session ID</string>
|
||||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scanner un Code QR</string>
|
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scanner un QR Code</string>
|
||||||
<string name="activity_create_private_chat_scan_qr_code_explanation">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.</string>
|
<string name="activity_create_private_chat_scan_qr_code_explanation">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.</string>
|
||||||
<string name="fragment_enter_public_key_edit_text_hint">Entrer un Session ID ou un nom ONS</string>
|
<string name="fragment_enter_public_key_edit_text_hint">Entrer un Session ID ou un nom ONS</string>
|
||||||
<string name="fragment_enter_public_key_explanation">Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.</string>
|
<string name="fragment_enter_public_key_explanation">Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.</string>
|
||||||
<string name="fragment_enter_public_key_error_message">Veuillez vérifier le Session ID ou le nom ONS et réessayer.</string>
|
<string name="fragment_enter_public_key_error_message">Veuillez vérifier le Session ID ou le nom ONS et réessayer.</string>
|
||||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session a besoin d\'accéder à l\'appareil photo pour scanner les codes QR</string>
|
<string name="fragment_scan_qr_code_camera_access_explanation">Session a besoin d\'accéder à l\'appareil photo pour scanner les QR codes</string>
|
||||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Autoriser l\'accès</string>
|
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Autoriser l\'accès</string>
|
||||||
<string name="activity_create_closed_group_title">Nouveau groupe privé</string>
|
<string name="activity_create_closed_group_title">Nouveau groupe privé</string>
|
||||||
<string name="activity_create_closed_group_edit_text_hint">Saisissez un nom de groupe</string>
|
<string name="activity_create_closed_group_edit_text_hint">Saisissez un nom de groupe</string>
|
||||||
@ -591,7 +618,7 @@
|
|||||||
<string name="activity_join_public_chat_title">Joindre un groupe public</string>
|
<string name="activity_join_public_chat_title">Joindre un groupe public</string>
|
||||||
<string name="activity_join_public_chat_error">Impossible de rejoindre le groupe</string>
|
<string name="activity_join_public_chat_error">Impossible de rejoindre le groupe</string>
|
||||||
<string name="activity_join_public_chat_enter_group_url_tab_title">URL du groupe public</string>
|
<string name="activity_join_public_chat_enter_group_url_tab_title">URL du groupe public</string>
|
||||||
<string name="activity_join_public_chat_scan_qr_code_tab_title">Scannez le code QR</string>
|
<string name="activity_join_public_chat_scan_qr_code_tab_title">Scanner le QR Code</string>
|
||||||
<string name="activity_join_public_chat_scan_qr_code_explanation">Scannez le code QR du groupe public que vous souhaitez rejoindre</string>
|
<string name="activity_join_public_chat_scan_qr_code_explanation">Scannez le code QR du groupe public que vous souhaitez rejoindre</string>
|
||||||
<string name="fragment_enter_chat_url_edit_text_hint">Saisissez une URL de groupe public</string>
|
<string name="fragment_enter_chat_url_edit_text_hint">Saisissez une URL de groupe public</string>
|
||||||
<string name="activity_settings_title">Paramètres</string>
|
<string name="activity_settings_title">Paramètres</string>
|
||||||
@ -600,6 +627,7 @@
|
|||||||
<string name="activity_settings_display_name_too_long_error">Veuillez choisir un nom d\'utilisateur plus court</string>
|
<string name="activity_settings_display_name_too_long_error">Veuillez choisir un nom d\'utilisateur plus court</string>
|
||||||
<string name="activity_settings_privacy_button_title">Confidentialité</string>
|
<string name="activity_settings_privacy_button_title">Confidentialité</string>
|
||||||
<string name="activity_settings_notifications_button_title">Notifications</string>
|
<string name="activity_settings_notifications_button_title">Notifications</string>
|
||||||
|
<string name="activity_settings_message_requests_button_title">Demandes de message</string>
|
||||||
<string name="activity_settings_chats_button_title">Conversations</string>
|
<string name="activity_settings_chats_button_title">Conversations</string>
|
||||||
<string name="activity_settings_devices_button_title">Appareils reliés</string>
|
<string name="activity_settings_devices_button_title">Appareils reliés</string>
|
||||||
<string name="activity_settings_invite_button_title">Inviter un ami</string>
|
<string name="activity_settings_invite_button_title">Inviter un ami</string>
|
||||||
@ -612,25 +640,39 @@
|
|||||||
<string name="activity_notification_settings_style_section_title">Style de notification</string>
|
<string name="activity_notification_settings_style_section_title">Style de notification</string>
|
||||||
<string name="activity_notification_settings_content_section_title">Contenu de notification</string>
|
<string name="activity_notification_settings_content_section_title">Contenu de notification</string>
|
||||||
<string name="activity_privacy_settings_title">Confidentialité</string>
|
<string name="activity_privacy_settings_title">Confidentialité</string>
|
||||||
|
<string name="activity_conversations_settings_title">Conversations</string>
|
||||||
|
<string name="activity_help_settings_title">Aide</string>
|
||||||
|
<string name="activity_help_settings__report_bug_title">Signaler un bug</string>
|
||||||
|
<string name="activity_help_settings__report_bug_summary">Exportez vos logs, puis télécharger le fichier au service d\'aide de Session.</string>
|
||||||
|
<string name="activity_help_settings__translate_session">Traduire Session</string>
|
||||||
|
<string name="activity_help_settings__feedback">Nous aimerions avoir votre avis</string>
|
||||||
|
<string name="activity_help_settings__faq">FAQ</string>
|
||||||
|
<string name="activity_help_settings__support">Assistance</string>
|
||||||
|
<string name="activity_help_settings__export_logs">Exporter les journaux</string>
|
||||||
<string name="preferences_notifications_strategy_category_title">Stratégie de notification</string>
|
<string name="preferences_notifications_strategy_category_title">Stratégie de notification</string>
|
||||||
<string name="preferences_notifications_strategy_category_fast_mode_title">Utiliser le Mode Rapide</string>
|
<string name="preferences_notifications_strategy_category_fast_mode_title">Utiliser le Mode Rapide</string>
|
||||||
<string name="preferences_notifications_strategy_category_fast_mode_summary">Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google.</string>
|
<string name="preferences_notifications_strategy_category_fast_mode_summary">Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google.</string>
|
||||||
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Modifier le nom</string>
|
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Modifier le nom</string>
|
||||||
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Déconnecter l\'appareil</string>
|
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Déconnecter l\'appareil</string>
|
||||||
<string name="dialog_seed_title">Votre phrase de récupération</string>
|
<string name="dialog_seed_title">Votre phrase de récupération</string>
|
||||||
<string name="dialog_seed_explanation">Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil.</string>
|
<string name="dialog_seed_explanation">Vous pouvez utiliser votre phrase de récupération pour restaurer votre compte ou relier un appareil.</string>
|
||||||
<string name="dialog_clear_all_data_title">Effacer toutes les données</string>
|
<string name="dialog_clear_all_data_title">Effacer toutes les données</string>
|
||||||
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
|
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
|
||||||
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
|
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
|
||||||
|
<string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_only">Effacer l\'appareil uniquement</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_and_network">Effacer l\'appareil et le réseau</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string>
|
||||||
|
<string name="dialog_clear_all_data_clear">Effacer</string>
|
||||||
<string name="dialog_clear_all_data_local_only">Effacer seulement</string>
|
<string name="dialog_clear_all_data_local_only">Effacer seulement</string>
|
||||||
<string name="dialog_clear_all_data_clear_network">Compte complet</string>
|
<string name="dialog_clear_all_data_clear_network">Compte complet</string>
|
||||||
<string name="activity_qr_code_title">Code QR</string>
|
<string name="activity_qr_code_title">QR Code</string>
|
||||||
<string name="activity_qr_code_view_my_qr_code_tab_title">Afficher mon code QR</string>
|
<string name="activity_qr_code_view_my_qr_code_tab_title">Afficher mon QR code</string>
|
||||||
<string name="activity_qr_code_view_scan_qr_code_tab_title">Scanner le code QR</string>
|
<string name="activity_qr_code_view_scan_qr_code_tab_title">Scanner le QR Code</string>
|
||||||
<string name="activity_qr_code_view_scan_qr_code_explanation">Scannez le code QR d\'un autre utilisateur pour démarrer une session</string>
|
<string name="activity_qr_code_view_scan_qr_code_explanation">Scannez le QR code d\'un autre utilisateur pour démarrer une session</string>
|
||||||
<string name="fragment_view_my_qr_code_title">Scannez-moi</string>
|
<string name="fragment_view_my_qr_code_title">Scannez-moi</string>
|
||||||
<string name="fragment_view_my_qr_code_explanation">Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.</string>
|
<string name="fragment_view_my_qr_code_explanation">Ceci est votre QR code. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.</string>
|
||||||
<string name="fragment_view_my_qr_code_share_title">Partager le code QR</string>
|
<string name="fragment_view_my_qr_code_share_title">Partager le QR code</string>
|
||||||
<string name="fragment_contact_selection_contacts_title">Contacts</string>
|
<string name="fragment_contact_selection_contacts_title">Contacts</string>
|
||||||
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
|
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
|
||||||
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
|
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
|
||||||
@ -664,8 +706,8 @@
|
|||||||
<string name="activity_link_device_skip_prompt">Cela prend un certain temps, voulez-vous passer ?</string>
|
<string name="activity_link_device_skip_prompt">Cela prend un certain temps, voulez-vous passer ?</string>
|
||||||
<string name="activity_link_device_link_device">Relier un appareil</string>
|
<string name="activity_link_device_link_device">Relier un appareil</string>
|
||||||
<string name="activity_link_device_recovery_phrase">Phrase de récupération</string>
|
<string name="activity_link_device_recovery_phrase">Phrase de récupération</string>
|
||||||
<string name="activity_link_device_scan_qr_code">Scannez le code QR</string>
|
<string name="activity_link_device_scan_qr_code">Scanner le QR Code</string>
|
||||||
<string name="activity_link_device_qr_message">Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre code QR.</string>
|
<string name="activity_link_device_qr_message">Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre QR code.</string>
|
||||||
<string name="activity_join_public_chat_join_rooms">Ou rejoignez l\'un(e) de ceux-ci…</string>
|
<string name="activity_join_public_chat_join_rooms">Ou rejoignez l\'un(e) de ceux-ci…</string>
|
||||||
<string name="activity_pn_mode_message_notifications">Notifications de message</string>
|
<string name="activity_pn_mode_message_notifications">Notifications de message</string>
|
||||||
<string name="activity_pn_mode_explanation">Session peut vous avertir de la présence de nouveaux messages de deux façons.</string>
|
<string name="activity_pn_mode_explanation">Session peut vous avertir de la présence de nouveaux messages de deux façons.</string>
|
||||||
@ -694,6 +736,7 @@
|
|||||||
<string name="dialog_download_explanation">Êtes-vous sûr de vouloir télécharger le média envoyé par %s ?</string>
|
<string name="dialog_download_explanation">Êtes-vous sûr de vouloir télécharger le média envoyé par %s ?</string>
|
||||||
<string name="dialog_download_button_title">Télécharger</string>
|
<string name="dialog_download_button_title">Télécharger</string>
|
||||||
<string name="activity_conversation_blocked_banner_text">%s est bloqué. Débloquer ?</string>
|
<string name="activity_conversation_blocked_banner_text">%s est bloqué. Débloquer ?</string>
|
||||||
|
<string name="activity_conversation_block_user">Bloquer l\'utilisateur</string>
|
||||||
<string name="activity_conversation_attachment_prep_failed">La préparation de la pièce jointe pour l\'envoi a échoué.</string>
|
<string name="activity_conversation_attachment_prep_failed">La préparation de la pièce jointe pour l\'envoi a échoué.</string>
|
||||||
<string name="media">Médias</string>
|
<string name="media">Médias</string>
|
||||||
<string name="UntrustedAttachmentView_download_attachment">Touchez pour télécharger %s</string>
|
<string name="UntrustedAttachmentView_download_attachment">Touchez pour télécharger %s</string>
|
||||||
@ -711,6 +754,116 @@
|
|||||||
<string name="activity_settings_support">Journal de débogage</string>
|
<string name="activity_settings_support">Journal de débogage</string>
|
||||||
<string name="dialog_share_logs_title">Partager les logs</string>
|
<string name="dialog_share_logs_title">Partager les logs</string>
|
||||||
<string name="dialog_share_logs_explanation">Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ?</string>
|
<string name="dialog_share_logs_explanation">Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ?</string>
|
||||||
<string name="conversation_pin">Code pin</string>
|
<string name="conversation_pin">Épingler</string>
|
||||||
<string name="conversation_unpin">Désépingler</string>
|
<string name="conversation_unpin">Désépingler</string>
|
||||||
|
<string name="mark_all_as_read">Tout marquer comme lu</string>
|
||||||
|
<string name="global_search_contacts_groups">Contacts et Groupes</string>
|
||||||
|
<string name="global_search_messages">Messages</string>
|
||||||
|
<string name="activity_message_requests_title">Demandes de message</string>
|
||||||
|
<string name="message_requests_send_notice">Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message et révélera votre ID de session.</string>
|
||||||
|
<string name="accept">Accepter</string>
|
||||||
|
<string name="decline">Refuser</string>
|
||||||
|
<string name="message_requests_clear_all">Effacer tout</string>
|
||||||
|
<string name="message_requests_decline_message">Êtes-vous sûr de vouloir refuser cette demande de message ?</string>
|
||||||
|
<string name="message_requests_block_message">Êtes-vous sûr de vouloir supprimer cette demande de message ?</string>
|
||||||
|
<string name="message_requests_deleted">Demande de message supprimée</string>
|
||||||
|
<string name="message_requests_clear_all_message">Êtes-vous sûr de vouloir supprimer toutes les demandes de message ?</string>
|
||||||
|
<string name="message_requests_cleared">Demandes de message supprimées</string>
|
||||||
|
<string name="message_requests_accepted">Votre demande de message a été acceptée.</string>
|
||||||
|
<string name="message_requests_pending">Votre demande de message est en attente.</string>
|
||||||
|
<string name="message_request_empty_state_message">Aucune demande de message en attente</string>
|
||||||
|
<string name="NewConversationButton_SessionTooltip">Message privé</string>
|
||||||
|
<string name="NewConversationButton_ClosedGroupTooltip">Groupes privés</string>
|
||||||
|
<string name="NewConversationButton_OpenGroupTooltip">Groupe public</string>
|
||||||
|
<string name="message_requests_notification">Vous avez une nouvelle demande de message</string>
|
||||||
|
<string name="CallNotificationBuilder_connecting">Connexion…</string>
|
||||||
|
<string name="NotificationBarManager__incoming_signal_call">Appel entrant</string>
|
||||||
|
<string name="NotificationBarManager__deny_call">Refuser l’appel</string>
|
||||||
|
<string name="NotificationBarManager__answer_call">Répondre à l’appel</string>
|
||||||
|
<string name="NotificationBarManager_call_in_progress">Appel en cours</string>
|
||||||
|
<string name="NotificationBarManager__cancel_call">Annuler l’appel</string>
|
||||||
|
<string name="NotificationBarManager__establishing_signal_call">Établissement de l\'appel</string>
|
||||||
|
<string name="NotificationBarManager__end_call">Raccrocher</string>
|
||||||
|
<string name="accept_call">Accepter l\'appel</string>
|
||||||
|
<string name="decline_call">Refuser l\'appel</string>
|
||||||
|
<string name="preferences__voice_video_calls">Appels vocaux et vidéos</string>
|
||||||
|
<string name="preferences__calls_beta">Appels (Bêta)</string>
|
||||||
|
<string name="preferences__allow_access_voice_video">Active les appels vocaux et vidéo vers et depuis d\'autres utilisateurs.</string>
|
||||||
|
<string name="dialog_voice_video_title">Appels vocaux / vidéo</string>
|
||||||
|
<string name="dialog_voice_video_message">La version actuelle des appels vocaux/vidéo exposera votre adresse IP aux serveurs de la Fondation Oxen et aux utilisateurs appelés</string>
|
||||||
|
<string name="CallNotificationBuilder_first_call_title">Appel Manqué</string>
|
||||||
|
<string name="CallNotificationBuilder_first_call_message">Vous avez manqué un appel car vous devez activer la permission « Appels vocaux et vidéo » dans les paramètres de confidentialité.</string>
|
||||||
|
<string name="WebRtcCallActivity_Session_Call">Appel Session</string>
|
||||||
|
<string name="WebRtcCallActivity_Reconnecting">Reconnexion…</string>
|
||||||
|
<string name="CallNotificationBuilder_system_notification_title">Notifications</string>
|
||||||
|
<string name="CallNotificationBuilder_system_notification_message">Les notifications désactivées vous empêcheront de recevoir des appels, aller dans les paramètres de notification de session?</string>
|
||||||
|
<string name="dismiss">Rejeter</string>
|
||||||
|
<string name="activity_settings_conversations_button_title">Conversations</string>
|
||||||
|
<string name="activity_settings_message_appearance_button_title">Apparence</string>
|
||||||
|
<string name="activity_settings_help_button">Aide</string>
|
||||||
|
<string name="activity_appearance_themes_category">Thèmes</string>
|
||||||
|
<string name="ocean_dark_theme_name">Océan sombre</string>
|
||||||
|
<string name="classic_dark_theme_name">Sombre classique</string>
|
||||||
|
<string name="ocean_light_theme_name">Océan lumineux</string>
|
||||||
|
<string name="classic_light_theme_name">Clair classique</string>
|
||||||
|
<string name="activity_appearance_primary_color_category">Couleur principale</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__this_message">Ce message</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__recently_used">Fréquemment Utilisés</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people">Émoticônes et personnes</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__nature" comment="Heading for an emoji list's category">Nature</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__food" comment="Heading for an emoji list's category">Nourriture</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__activities">Activités</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__places" comment="Heading for an emoji list's category">Voyage</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__objects">Objets</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__symbols">Symboles</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__flags">Drapeaux</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__emoticons">Emoticônes</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__no_results_found">Aucun résultat trouvé</string>
|
||||||
|
<!-- ReactionsBottomSheetDialogFragment -->
|
||||||
|
<string name="ReactionsBottomSheetDialogFragment_all">Tous · %1$d</string>
|
||||||
|
<!-- ReactionsConversationView -->
|
||||||
|
<string name="ReactionsConversationView_plus">+%1$d</string>
|
||||||
|
<!-- ReactionsRecipientAdapter -->
|
||||||
|
<string name="ReactionsRecipientAdapter_you">Vous</string>
|
||||||
|
<string name="reaction_notification">%1$s a réagi à un message %2$s</string>
|
||||||
|
<string name="ReactionsConversationView_show_less">Masquer les détails</string>
|
||||||
|
<string name="KeyboardPagerFragment_search_emoji">Rechercher un émoticône</string>
|
||||||
|
<string name="KeyboardPagerfragment_back_to_emoji">Retour à l\'émoticône</string>
|
||||||
|
<string name="KeyboardPagerfragment_clear_search_entry">Effacer la recherche</string>
|
||||||
|
<string name="activity_appearance_follow_system_category">Thème sombre automatique</string>
|
||||||
|
<string name="activity_appearance_follow_system_explanation">Faire correspondre aux paramètres systèmes</string>
|
||||||
|
<string name="go_to_device_notification_settings">Accédez aux paramètres de notifications de l\'appareil</string>
|
||||||
|
<string name="blocked_contacts_title">Contacts bloqués</string>
|
||||||
|
<string name="blocked_contacts_empty_state">Vous n\'avez aucun contact bloqué</string>
|
||||||
|
<string name="Unblock_dialog__title_single">Débloquer %s</string>
|
||||||
|
<string name="Unblock_dialog__title_multiple">Débloquer les utilisateurs</string>
|
||||||
|
<string name="Unblock_dialog__message">Êtes-vous sûr·e de vouloir débloquer %s ?</string>
|
||||||
|
<plurals name="Unblock_dialog__message_multiple_overflow">
|
||||||
|
<item quantity="one">et %d autre</item>
|
||||||
|
<item quantity="other">et %d autres</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="ReactionsRecipientAdapter_other_reactors">
|
||||||
|
<item quantity="one">Et %1$d autre a réagi %2$s à ce message</item>
|
||||||
|
<item quantity="other">Et %1$d autres ont réagi %2$s à ce message</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="dialog_new_conversation_title">Nouvelle conversation</string>
|
||||||
|
<string name="dialog_new_message_title">Nouveau message</string>
|
||||||
|
<string name="activity_create_group_title">Créer un groupe</string>
|
||||||
|
<string name="dialog_join_community_title">Rejoindre la communauté</string>
|
||||||
|
<string name="new_conversation_contacts_title">Contacts</string>
|
||||||
|
<string name="new_conversation_unknown_contacts_section_title">Inconnu·e</string>
|
||||||
|
<string name="fragment_enter_public_key_prompt">Commencez une nouvelle conversation en entrant l\'ID Session de quelqu\'un ou en lui partageant votre ID Session.</string>
|
||||||
|
<string name="activity_create_group_create_button_title">Créer</string>
|
||||||
|
<string name="search_contacts_hint">Rechercher parmi les contacts</string>
|
||||||
|
<string name="activity_join_public_chat_enter_community_url_tab_title">URL de la communauté</string>
|
||||||
|
<string name="fragment_enter_community_url_edit_text_hint">Entrez l\'URL de la communauté</string>
|
||||||
|
<string name="fragment_enter_community_url_join_button_title">Rejoindre</string>
|
||||||
|
<string name="new_conversation_dialog_back_button_content_description">Revenir en arrière</string>
|
||||||
|
<string name="new_conversation_dialog_close_button_content_description">Fermer la fenêtre</string>
|
||||||
|
<string name="ErrorNotifier_migration">Échec de la mise à jour de la base de données</string>
|
||||||
|
<string name="ErrorNotifier_migration_downgrade">Veuillez contacter le support pour signaler l\'erreur.</string>
|
||||||
|
<string name="delivery_status_sending">Envoi</string>
|
||||||
|
<string name="delivery_status_read">Lu</string>
|
||||||
|
<string name="delivery_status_sent">Envoyé</string>
|
||||||
|
<string name="delivery_status_failed">Échec d’envoi</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Session</string>
|
|
||||||
<string name="yes">Oui</string>
|
<string name="yes">Oui</string>
|
||||||
<string name="no">Non</string>
|
<string name="no">Non</string>
|
||||||
<string name="delete">Supprimer</string>
|
<string name="delete">Supprimer</string>
|
||||||
<string name="ban">Bannir</string>
|
<string name="ban">Bannir</string>
|
||||||
|
<string name="please_wait">Veuillez patienter…</string>
|
||||||
<string name="save">Enregistrer</string>
|
<string name="save">Enregistrer</string>
|
||||||
<string name="note_to_self">Note à mon intention</string>
|
<string name="note_to_self">Note à mon intention</string>
|
||||||
<string name="version_s">Version %s</string>
|
<string name="version_s">Version %s</string>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</plurals>
|
</plurals>
|
||||||
<string name="ApplicationPreferencesActivity_delete_all_old_messages_now">Supprimer tous les anciens messages maintenant ?</string>
|
<string name="ApplicationPreferencesActivity_delete_all_old_messages_now">Supprimer tous les anciens messages maintenant ?</string>
|
||||||
<plurals name="ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages">
|
<plurals name="ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages">
|
||||||
<item quantity="one">Cela va immédiatement réduire toutes les conversations pour qu’il ne reste que le message le plus récent.</item>
|
<item quantity="one">Cela réduira immédiatement toutes les conversations au message le plus récent.</item>
|
||||||
<item quantity="other">Cela réduira immédiatement toutes les conversations aux %d messages les plus récents.</item>
|
<item quantity="other">Cela réduira immédiatement toutes les conversations aux %d messages les plus récents.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="ApplicationPreferencesActivity_delete">Supprimer</string>
|
<string name="ApplicationPreferencesActivity_delete">Supprimer</string>
|
||||||
@ -63,6 +63,7 @@
|
|||||||
<string name="ConversationActivity_muted_until_date">Son désactivé jusqu\'à %1$s</string>
|
<string name="ConversationActivity_muted_until_date">Son désactivé jusqu\'à %1$s</string>
|
||||||
<string name="ConversationActivity_muted_forever">En sourdine</string>
|
<string name="ConversationActivity_muted_forever">En sourdine</string>
|
||||||
<string name="ConversationActivity_member_count">%1$d membres</string>
|
<string name="ConversationActivity_member_count">%1$d membres</string>
|
||||||
|
<string name="ConversationActivity_active_member_count">%1$d membres actifs</string>
|
||||||
<string name="ConversationActivity_open_group_guidelines">Règles de la communauté</string>
|
<string name="ConversationActivity_open_group_guidelines">Règles de la communauté</string>
|
||||||
<string name="ConversationActivity_invalid_recipient">Le destinataire est invalide !</string>
|
<string name="ConversationActivity_invalid_recipient">Le destinataire est invalide !</string>
|
||||||
<string name="ConversationActivity_added_to_home_screen">Ajouté à l’écran d’accueil</string>
|
<string name="ConversationActivity_added_to_home_screen">Ajouté à l’écran d’accueil</string>
|
||||||
@ -82,7 +83,9 @@
|
|||||||
<string name="ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage">Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos.</string>
|
<string name="ConversationActivity_to_send_photos_and_video_allow_signal_access_to_storage">Session a besoin d\'un accès au stockage pour envoyer des photos et des vidéos.</string>
|
||||||
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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.</string>
|
<string name="ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video">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.</string>
|
||||||
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos</string>
|
<string name="ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video">Session a besoin de l’autorisation Appareil photo pour prendre des photos ou des vidéos</string>
|
||||||
<string name="ConversationActivity_search_position">%1$d de %2$d</string>
|
<string name="ConversationActivity_search_position">%1$d sur %2$d</string>
|
||||||
|
<string name="ConversationActivity_call_title">Autorisations d\'appel requises</string>
|
||||||
|
<string name="ConversationActivity_call_prompt">Vous pouvez activer la permission \"Appels vocaux et vidéo\" dans les paramètres de confidentialité.</string>
|
||||||
<!-- ConversationFragment -->
|
<!-- ConversationFragment -->
|
||||||
<plurals name="ConversationFragment_delete_selected_messages">
|
<plurals name="ConversationFragment_delete_selected_messages">
|
||||||
<item quantity="one">Supprimer le message sélectionné ?</item>
|
<item quantity="one">Supprimer le message sélectionné ?</item>
|
||||||
@ -99,13 +102,17 @@
|
|||||||
<item quantity="other">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 ?</item>
|
<item quantity="other">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 ?</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
|
<plurals name="ConversationFragment_error_while_saving_attachments_to_sd_card">
|
||||||
<item quantity="one">Erreur d’enregistrement de la pièce jointe dans la mémoire !</item>
|
<item quantity="one">Erreur lors de l’enregistrement de la pièce jointe dans la mémoire !</item>
|
||||||
<item quantity="other">Erreur d’enregistrement des pièces jointes dans la mémoire !</item>
|
<item quantity="other">Erreur d’enregistrement des pièces jointes dans la mémoire !</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="ConversationFragment_saving_n_attachments">
|
<plurals name="ConversationFragment_saving_n_attachments">
|
||||||
<item quantity="one">Enregistrement de la pièce jointe</item>
|
<item quantity="one">Enregistrement de la pièce jointe</item>
|
||||||
<item quantity="other">Enregistrement de %1$d pièces jointes</item>
|
<item quantity="other">Enregistrement de %1$d pièces jointes</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="ConversationFragment_saving_n_attachments_to_sd_card">
|
||||||
|
<item quantity="one">Enregistrement de la pièce jointe dans la mémoire…</item>
|
||||||
|
<item quantity="other">Enregistrement de %1$d pièces jointes dans la mémoire…</item>
|
||||||
|
</plurals>
|
||||||
<!-- CreateProfileActivity -->
|
<!-- CreateProfileActivity -->
|
||||||
<string name="CreateProfileActivity_profile_photo">Photo de profil</string>
|
<string name="CreateProfileActivity_profile_photo">Photo de profil</string>
|
||||||
<!-- CustomDefaultPreference -->
|
<!-- CustomDefaultPreference -->
|
||||||
@ -156,7 +163,7 @@
|
|||||||
<!-- MediaPickerActivity -->
|
<!-- MediaPickerActivity -->
|
||||||
<string name="MediaPickerActivity_send_to">Envoyer à %s</string>
|
<string name="MediaPickerActivity_send_to">Envoyer à %s</string>
|
||||||
<!-- MediaSendActivity -->
|
<!-- MediaSendActivity -->
|
||||||
<string name="MediaSendActivity_add_a_caption">Ajouter un légende…</string>
|
<string name="MediaSendActivity_add_a_caption">Ajouter une légende...</string>
|
||||||
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">Un élément a été supprimé, car il dépassait la taille limite</string>
|
<string name="MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit">Un élément a été supprimé, car il dépassait la taille limite</string>
|
||||||
<string name="MediaSendActivity_camera_unavailable">L’appareil photo n’est pas disponible</string>
|
<string name="MediaSendActivity_camera_unavailable">L’appareil photo n’est pas disponible</string>
|
||||||
<string name="MediaSendActivity_message_to_s">Message à %s</string>
|
<string name="MediaSendActivity_message_to_s">Message à %s</string>
|
||||||
@ -191,6 +198,7 @@
|
|||||||
<string name="Slide_video">Vidéo</string>
|
<string name="Slide_video">Vidéo</string>
|
||||||
<!-- SmsMessageRecord -->
|
<!-- SmsMessageRecord -->
|
||||||
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Vous avez reçu un message d’échange de clés corrompu !</string>
|
<string name="SmsMessageRecord_received_corrupted_key_exchange_message">Vous avez reçu un message d’échange de clés corrompu !</string>
|
||||||
|
<string name="SmsMessageRecord_received_key_exchange_message_for_invalid_protocol_version"> Le message d\'échange de clé reçu est pour une version du protocole invalide. </string>
|
||||||
<string name="SmsMessageRecord_received_message_with_new_safety_number_tap_to_process">Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher.</string>
|
<string name="SmsMessageRecord_received_message_with_new_safety_number_tap_to_process">Vous avez reçu un message avec un nouveau numéro de sécurité. Touchez pour le traiter et l’afficher.</string>
|
||||||
<string name="SmsMessageRecord_secure_session_reset">Vous avez réinitialisé la session sécurisée.</string>
|
<string name="SmsMessageRecord_secure_session_reset">Vous avez réinitialisé la session sécurisée.</string>
|
||||||
<string name="SmsMessageRecord_secure_session_reset_s">%s a réinitialisé la session sécurisée.</string>
|
<string name="SmsMessageRecord_secure_session_reset_s">%s a réinitialisé la session sécurisée.</string>
|
||||||
@ -201,7 +209,7 @@
|
|||||||
<string name="ThreadRecord_secure_session_reset">La session sécurisée a été réinitialisée.</string>
|
<string name="ThreadRecord_secure_session_reset">La session sécurisée a été réinitialisée.</string>
|
||||||
<string name="ThreadRecord_draft">Brouillon :</string>
|
<string name="ThreadRecord_draft">Brouillon :</string>
|
||||||
<string name="ThreadRecord_called">Vous avez appelé</string>
|
<string name="ThreadRecord_called">Vous avez appelé</string>
|
||||||
<string name="ThreadRecord_called_you">Vous a appelé</string>
|
<string name="ThreadRecord_called_you">Vous a appelé·e</string>
|
||||||
<string name="ThreadRecord_missed_call">Appel manqué</string>
|
<string name="ThreadRecord_missed_call">Appel manqué</string>
|
||||||
<string name="ThreadRecord_media_message">Message multimédia</string>
|
<string name="ThreadRecord_media_message">Message multimédia</string>
|
||||||
<string name="ThreadRecord_s_is_on_signal">%s est sur Session !</string>
|
<string name="ThreadRecord_s_is_on_signal">%s est sur Session !</string>
|
||||||
@ -304,7 +312,7 @@
|
|||||||
<string name="conversation_activity__send">Envoyer</string>
|
<string name="conversation_activity__send">Envoyer</string>
|
||||||
<string name="conversation_activity__compose_description">Rédaction d’un message</string>
|
<string name="conversation_activity__compose_description">Rédaction d’un message</string>
|
||||||
<string name="conversation_activity__emoji_toggle_description">Afficher, masquer le clavier des émojis</string>
|
<string name="conversation_activity__emoji_toggle_description">Afficher, masquer le clavier des émojis</string>
|
||||||
<string name="conversation_activity__attachment_thumbnail">Imagette de pièces jointes</string>
|
<string name="conversation_activity__attachment_thumbnail">Vignette de pièce jointe</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution</string>
|
<string name="conversation_activity__quick_attachment_drawer_toggle_camera_description">Afficher, masquer le tiroir permettant de lancer l’appareil photo à basse résolution</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Enregistrer et envoyer une pièce jointe audio</string>
|
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Enregistrer et envoyer une pièce jointe audio</string>
|
||||||
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Verrouiller l’enregistrement de pièces jointes audio</string>
|
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Verrouiller l’enregistrement de pièces jointes audio</string>
|
||||||
@ -378,9 +386,9 @@
|
|||||||
<string name="arrays__settings_default">Valeur par défaut</string>
|
<string name="arrays__settings_default">Valeur par défaut</string>
|
||||||
<string name="arrays__enabled">Activé</string>
|
<string name="arrays__enabled">Activé</string>
|
||||||
<string name="arrays__disabled">Désactivé</string>
|
<string name="arrays__disabled">Désactivé</string>
|
||||||
<string name="arrays__name_and_message">Nom et message</string>
|
<string name="arrays__name_and_message">Nom et Contenu</string>
|
||||||
<string name="arrays__name_only">Nom seulement</string>
|
<string name="arrays__name_only">Nom uniquement</string>
|
||||||
<string name="arrays__no_name_or_message">Aucun nom ni message</string>
|
<string name="arrays__no_name_or_message">Aucun nom ni contenu</string>
|
||||||
<string name="arrays__images">Images</string>
|
<string name="arrays__images">Images</string>
|
||||||
<string name="arrays__audio">Son</string>
|
<string name="arrays__audio">Son</string>
|
||||||
<string name="arrays__video">Vidéo</string>
|
<string name="arrays__video">Vidéo</string>
|
||||||
@ -398,9 +406,14 @@
|
|||||||
<item quantity="other">%d heures</item>
|
<item quantity="other">%d heures</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<!-- preferences.xml -->
|
<!-- preferences.xml -->
|
||||||
<string name="preferences__pref_enter_sends_title">La touche Entrée envoie</string>
|
<string name="preferences__pref_enter_sends_title">Envoyer avec bouton Entrée</string>
|
||||||
<string name="preferences__send_link_previews">Envoyer des aperçus de liens</string>
|
<string name="preferences__pref_enter_sends_summary">Appuyer sur la touche Entrée enverra un message au lieu de commencer une nouvelle ligne.</string>
|
||||||
<string name="preferences__previews_are_supported_for">Les aperçus sont pris en charge pour les liens Imgur, Instagram, Pinterest, Reddit et YouTube</string>
|
<string name="preferences__send_link_previews">Envoyer les aperçus des liens</string>
|
||||||
|
<string name="preferences__link_previews">Aperçus des liens</string>
|
||||||
|
<string name="preferences__link_previews_summary">Générer les aperçus des liens pour les URLs supportés.</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_category">Messages audio</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_title">Lire automatiquement les messages audios</string>
|
||||||
|
<string name="preferences__pref_autoplay_audio_summary">Lire automatiquement les messages audio consécutifs.</string>
|
||||||
<string name="preferences__screen_security">Sécurité de l’écran</string>
|
<string name="preferences__screen_security">Sécurité de l’écran</string>
|
||||||
<string name="preferences__disable_screen_security_to_allow_screen_shots">Bloquer les captures d’écran dans la liste des récents et dans l’appli</string>
|
<string name="preferences__disable_screen_security_to_allow_screen_shots">Bloquer les captures d’écran dans la liste des récents et dans l’appli</string>
|
||||||
<string name="preferences__notifications">Notifications</string>
|
<string name="preferences__notifications">Notifications</string>
|
||||||
@ -408,6 +421,7 @@
|
|||||||
<string name="preferences__led_color_unknown">Inconnue</string>
|
<string name="preferences__led_color_unknown">Inconnue</string>
|
||||||
<string name="preferences__pref_led_blink_title">Rythme de clignotement de la DEL</string>
|
<string name="preferences__pref_led_blink_title">Rythme de clignotement de la DEL</string>
|
||||||
<string name="preferences__sound">Son</string>
|
<string name="preferences__sound">Son</string>
|
||||||
|
<string name="preferences__in_app_sounds">Son à l\'ouverture de l\'application</string>
|
||||||
<string name="preferences__silent">Silencieux</string>
|
<string name="preferences__silent">Silencieux</string>
|
||||||
<string name="preferences__repeat_alerts">Répéter les alertes</string>
|
<string name="preferences__repeat_alerts">Répéter les alertes</string>
|
||||||
<string name="preferences__never">Jamais</string>
|
<string name="preferences__never">Jamais</string>
|
||||||
@ -436,18 +450,27 @@
|
|||||||
<string name="preferences__default">Valeur par défaut</string>
|
<string name="preferences__default">Valeur par défaut</string>
|
||||||
<string name="preferences__incognito_keyboard">Clavier incognito</string>
|
<string name="preferences__incognito_keyboard">Clavier incognito</string>
|
||||||
<string name="preferences__read_receipts">Accusés de lecture</string>
|
<string name="preferences__read_receipts">Accusés de lecture</string>
|
||||||
|
<string name="preferences__read_receipts_summary">Envoyer des accusés de lecture dans les conversations individuelles.</string>
|
||||||
<string name="preferences__typing_indicators">Indicateurs de saisie</string>
|
<string name="preferences__typing_indicators">Indicateurs de saisie</string>
|
||||||
<string name="preferences__if_typing_indicators_are_disabled_you_wont_be_able_to_see_typing_indicators">Si les indicateurs de saisie sont désactivés, vous ne serez pas en mesure de voir les indicateurs de saisie des autres.</string>
|
<string name="preferences__typing_indicators_summary">Voir et envoyer les indicateurs de saisie dans les conversations un à un.</string>
|
||||||
<string name="preferences__request_keyboard_to_disable_personalized_learning">Demander au clavier de désactiver l’apprentissage personnalisé</string>
|
<string name="preferences__request_keyboard_to_disable_personalized_learning">Demander au clavier de désactiver l’apprentissage personnalisé</string>
|
||||||
<string name="preferences__light_theme">Clair</string>
|
<string name="preferences__light_theme">Clair</string>
|
||||||
<string name="preferences__dark_theme">Sombre</string>
|
<string name="preferences__dark_theme">Sombre</string>
|
||||||
<string name="preferences_chats__message_trimming">Élagage des messages</string>
|
<string name="preferences_chats__message_trimming">Élagage des messages</string>
|
||||||
|
<string name="preferences_chats__message_trimming_title">Raccourcir les communautés</string>
|
||||||
|
<string name="preferences_chats__message_trimming_summary">Supprimer des messages de plus de 6 mois dans des communautés qui ont plus de 2 000 messages.</string>
|
||||||
<string name="preferences_advanced__use_system_emoji">Utiliser les émojis du système</string>
|
<string name="preferences_advanced__use_system_emoji">Utiliser les émojis du système</string>
|
||||||
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Désactiver la prise en charge des émojis intégrés à Session</string>
|
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Désactiver la prise en charge des émojis intégrés à Session</string>
|
||||||
|
<string name="preferences_app_protection__screen_security">Sécurité d\'écran</string>
|
||||||
<string name="preferences_chats__chats">Conversations</string>
|
<string name="preferences_chats__chats">Conversations</string>
|
||||||
<string name="preferences_notifications__messages">Messages</string>
|
<string name="preferences_notifications__messages">Messages</string>
|
||||||
<string name="preferences_notifications__in_chat_sounds">Sons des conversations</string>
|
<string name="preferences_notifications__in_chat_sounds">Sons des conversations</string>
|
||||||
|
<string name="preferences_notifications__content">Contenu de la notification</string>
|
||||||
|
<string name="preferences_notifications__content_message">Afficher :</string>
|
||||||
|
<string name="preferences_notifications__summary">Informations affichées dans les notifications.</string>
|
||||||
<string name="preferences_notifications__priority">Priorité</string>
|
<string name="preferences_notifications__priority">Priorité</string>
|
||||||
|
<string name="preferences_app_protection__screenshot_notifications">Notifications de capture d\'écran</string>
|
||||||
|
<string name="preferences_app_protected__screenshot_notifications_summary">Recevoir une notification lorsqu\'un contact prend une capture d\'écran d\'une conversation individuelle.</string>
|
||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
<!-- menus -->
|
<!-- menus -->
|
||||||
<!-- **************************************** -->
|
<!-- **************************************** -->
|
||||||
@ -460,7 +483,10 @@
|
|||||||
<string name="conversation_context__menu_ban_user">Bannir l\'utilisateur</string>
|
<string name="conversation_context__menu_ban_user">Bannir l\'utilisateur</string>
|
||||||
<string name="conversation_context__menu_ban_and_delete_all">Bannir et supprimer tout</string>
|
<string name="conversation_context__menu_ban_and_delete_all">Bannir et supprimer tout</string>
|
||||||
<string name="conversation_context__menu_resend_message">Renvoyer le message</string>
|
<string name="conversation_context__menu_resend_message">Renvoyer le message</string>
|
||||||
|
<string name="conversation_context__menu_reply">Répondre</string>
|
||||||
<string name="conversation_context__menu_reply_to_message">Répondre au message</string>
|
<string name="conversation_context__menu_reply_to_message">Répondre au message</string>
|
||||||
|
<string name="conversation_context__menu_call">Appeler</string>
|
||||||
|
<string name="conversation_context__menu_select">Sélectionner</string>
|
||||||
<!-- conversation_context_image -->
|
<!-- conversation_context_image -->
|
||||||
<string name="conversation_context_image__save_attachment">Enregistrer la pièce jointe</string>
|
<string name="conversation_context_image__save_attachment">Enregistrer la pièce jointe</string>
|
||||||
<!-- conversation_expiring_off -->
|
<!-- conversation_expiring_off -->
|
||||||
@ -513,8 +539,8 @@
|
|||||||
<string name="LocalBackupJob_creating_backup">Création de la sauvegarde…</string>
|
<string name="LocalBackupJob_creating_backup">Création de la sauvegarde…</string>
|
||||||
<string name="ProgressPreference_d_messages_so_far">%d messages pour l’instant</string>
|
<string name="ProgressPreference_d_messages_so_far">%d messages pour l’instant</string>
|
||||||
<string name="BackupUtil_never">Jamais</string>
|
<string name="BackupUtil_never">Jamais</string>
|
||||||
<string name="preferences_app_protection__screen_lock">Verrouillage de l’écran</string>
|
<string name="preferences_app_protection__screen_lock">Verrouiller Session</string>
|
||||||
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Verrouiller l’accès à Session avec le verrouillage de l’écran d’Android ou une empreinte</string>
|
<string name="preferences_app_protection__lock_signal_access_with_android_screen_lock_or_fingerprint">Nécessite une empreinte digitale, un code PIN, un schéma ou un mot de passe pour déverrouiller Session.</string>
|
||||||
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Délai d’inactivité avant verrouillage de l’écran</string>
|
<string name="preferences_app_protection__screen_lock_inactivity_timeout">Délai d’inactivité avant verrouillage de l’écran</string>
|
||||||
<string name="AppProtectionPreferenceFragment_none">Aucune</string>
|
<string name="AppProtectionPreferenceFragment_none">Aucune</string>
|
||||||
<!-- Conversation activity -->
|
<!-- Conversation activity -->
|
||||||
@ -522,6 +548,7 @@
|
|||||||
<!-- Session -->
|
<!-- Session -->
|
||||||
<string name="continue_2">Continuer</string>
|
<string name="continue_2">Continuer</string>
|
||||||
<string name="copy">Copier</string>
|
<string name="copy">Copier</string>
|
||||||
|
<string name="close">Fermer</string>
|
||||||
<string name="invalid_url">URL non valide</string>
|
<string name="invalid_url">URL non valide</string>
|
||||||
<string name="copied_to_clipboard">Copié dans le presse-papier</string>
|
<string name="copied_to_clipboard">Copié dans le presse-papier</string>
|
||||||
<string name="next">Suivant</string>
|
<string name="next">Suivant</string>
|
||||||
@ -573,12 +600,12 @@
|
|||||||
<string name="activity_path_resolving_progress">Contact en cours…</string>
|
<string name="activity_path_resolving_progress">Contact en cours…</string>
|
||||||
<string name="activity_create_private_chat_title">Nouvelle Session</string>
|
<string name="activity_create_private_chat_title">Nouvelle Session</string>
|
||||||
<string name="activity_create_private_chat_enter_session_id_tab_title">Saisir un Session ID</string>
|
<string name="activity_create_private_chat_enter_session_id_tab_title">Saisir un Session ID</string>
|
||||||
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scanner un Code QR</string>
|
<string name="activity_create_private_chat_scan_qr_code_tab_title">Scanner un QR Code</string>
|
||||||
<string name="activity_create_private_chat_scan_qr_code_explanation">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.</string>
|
<string name="activity_create_private_chat_scan_qr_code_explanation">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.</string>
|
||||||
<string name="fragment_enter_public_key_edit_text_hint">Entrer un Session ID ou un nom ONS</string>
|
<string name="fragment_enter_public_key_edit_text_hint">Entrer un Session ID ou un nom ONS</string>
|
||||||
<string name="fragment_enter_public_key_explanation">Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.</string>
|
<string name="fragment_enter_public_key_explanation">Les utilisateurs peuvent partager leur Session ID depuis les paramètres du compte ou en utilisant le code QR.</string>
|
||||||
<string name="fragment_enter_public_key_error_message">Veuillez vérifier le Session ID ou le nom ONS et réessayer.</string>
|
<string name="fragment_enter_public_key_error_message">Veuillez vérifier le Session ID ou le nom ONS et réessayer.</string>
|
||||||
<string name="fragment_scan_qr_code_camera_access_explanation">Session a besoin d\'accéder à l\'appareil photo pour scanner les codes QR</string>
|
<string name="fragment_scan_qr_code_camera_access_explanation">Session a besoin d\'accéder à l\'appareil photo pour scanner les QR codes</string>
|
||||||
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Autoriser l\'accès</string>
|
<string name="fragment_scan_qr_code_grant_camera_access_button_title">Autoriser l\'accès</string>
|
||||||
<string name="activity_create_closed_group_title">Nouveau groupe privé</string>
|
<string name="activity_create_closed_group_title">Nouveau groupe privé</string>
|
||||||
<string name="activity_create_closed_group_edit_text_hint">Saisissez un nom de groupe</string>
|
<string name="activity_create_closed_group_edit_text_hint">Saisissez un nom de groupe</string>
|
||||||
@ -591,7 +618,7 @@
|
|||||||
<string name="activity_join_public_chat_title">Joindre un groupe public</string>
|
<string name="activity_join_public_chat_title">Joindre un groupe public</string>
|
||||||
<string name="activity_join_public_chat_error">Impossible de rejoindre le groupe</string>
|
<string name="activity_join_public_chat_error">Impossible de rejoindre le groupe</string>
|
||||||
<string name="activity_join_public_chat_enter_group_url_tab_title">URL du groupe public</string>
|
<string name="activity_join_public_chat_enter_group_url_tab_title">URL du groupe public</string>
|
||||||
<string name="activity_join_public_chat_scan_qr_code_tab_title">Scannez le code QR</string>
|
<string name="activity_join_public_chat_scan_qr_code_tab_title">Scanner le QR Code</string>
|
||||||
<string name="activity_join_public_chat_scan_qr_code_explanation">Scannez le code QR du groupe public que vous souhaitez rejoindre</string>
|
<string name="activity_join_public_chat_scan_qr_code_explanation">Scannez le code QR du groupe public que vous souhaitez rejoindre</string>
|
||||||
<string name="fragment_enter_chat_url_edit_text_hint">Saisissez une URL de groupe public</string>
|
<string name="fragment_enter_chat_url_edit_text_hint">Saisissez une URL de groupe public</string>
|
||||||
<string name="activity_settings_title">Paramètres</string>
|
<string name="activity_settings_title">Paramètres</string>
|
||||||
@ -600,6 +627,7 @@
|
|||||||
<string name="activity_settings_display_name_too_long_error">Veuillez choisir un nom d\'utilisateur plus court</string>
|
<string name="activity_settings_display_name_too_long_error">Veuillez choisir un nom d\'utilisateur plus court</string>
|
||||||
<string name="activity_settings_privacy_button_title">Confidentialité</string>
|
<string name="activity_settings_privacy_button_title">Confidentialité</string>
|
||||||
<string name="activity_settings_notifications_button_title">Notifications</string>
|
<string name="activity_settings_notifications_button_title">Notifications</string>
|
||||||
|
<string name="activity_settings_message_requests_button_title">Demandes de message</string>
|
||||||
<string name="activity_settings_chats_button_title">Conversations</string>
|
<string name="activity_settings_chats_button_title">Conversations</string>
|
||||||
<string name="activity_settings_devices_button_title">Appareils reliés</string>
|
<string name="activity_settings_devices_button_title">Appareils reliés</string>
|
||||||
<string name="activity_settings_invite_button_title">Inviter un ami</string>
|
<string name="activity_settings_invite_button_title">Inviter un ami</string>
|
||||||
@ -612,25 +640,39 @@
|
|||||||
<string name="activity_notification_settings_style_section_title">Style de notification</string>
|
<string name="activity_notification_settings_style_section_title">Style de notification</string>
|
||||||
<string name="activity_notification_settings_content_section_title">Contenu de notification</string>
|
<string name="activity_notification_settings_content_section_title">Contenu de notification</string>
|
||||||
<string name="activity_privacy_settings_title">Confidentialité</string>
|
<string name="activity_privacy_settings_title">Confidentialité</string>
|
||||||
|
<string name="activity_conversations_settings_title">Conversations</string>
|
||||||
|
<string name="activity_help_settings_title">Aide</string>
|
||||||
|
<string name="activity_help_settings__report_bug_title">Signaler un bug</string>
|
||||||
|
<string name="activity_help_settings__report_bug_summary">Exportez vos logs, puis télécharger le fichier au service d\'aide de Session.</string>
|
||||||
|
<string name="activity_help_settings__translate_session">Traduire Session</string>
|
||||||
|
<string name="activity_help_settings__feedback">Nous aimerions avoir votre avis</string>
|
||||||
|
<string name="activity_help_settings__faq">FAQ</string>
|
||||||
|
<string name="activity_help_settings__support">Assistance</string>
|
||||||
|
<string name="activity_help_settings__export_logs">Exporter les journaux</string>
|
||||||
<string name="preferences_notifications_strategy_category_title">Stratégie de notification</string>
|
<string name="preferences_notifications_strategy_category_title">Stratégie de notification</string>
|
||||||
<string name="preferences_notifications_strategy_category_fast_mode_title">Utiliser le Mode Rapide</string>
|
<string name="preferences_notifications_strategy_category_fast_mode_title">Utiliser le Mode Rapide</string>
|
||||||
<string name="preferences_notifications_strategy_category_fast_mode_summary">Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google.</string>
|
<string name="preferences_notifications_strategy_category_fast_mode_summary">Vous serez averti de nouveaux messages de manière fiable et immédiate en utilisant les serveurs de notification de Google.</string>
|
||||||
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Modifier le nom</string>
|
<string name="fragment_device_list_bottom_sheet_change_name_button_title">Modifier le nom</string>
|
||||||
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Déconnecter l\'appareil</string>
|
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">Déconnecter l\'appareil</string>
|
||||||
<string name="dialog_seed_title">Votre phrase de récupération</string>
|
<string name="dialog_seed_title">Votre phrase de récupération</string>
|
||||||
<string name="dialog_seed_explanation">Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil.</string>
|
<string name="dialog_seed_explanation">Vous pouvez utiliser votre phrase de récupération pour restaurer votre compte ou relier un appareil.</string>
|
||||||
<string name="dialog_clear_all_data_title">Effacer toutes les données</string>
|
<string name="dialog_clear_all_data_title">Effacer toutes les données</string>
|
||||||
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
|
<string name="dialog_clear_all_data_explanation">Cela supprimera définitivement vos messages, vos sessions et vos contacts.</string>
|
||||||
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
|
<string name="dialog_clear_all_data_network_explanation">Souhaitez-vous effacer seulement cet appareil ou supprimer l\'ensemble de votre compte ?</string>
|
||||||
|
<string name="dialog_clear_all_data_message">Cela supprimera définitivement vos messages, sessions et contacts. Voulez-vous uniquement effacer cet appareil ou supprimer l\'intégralité de votre compte ?</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_only">Effacer l\'appareil uniquement</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_and_network">Effacer l\'appareil et le réseau</string>
|
||||||
|
<string name="dialog_clear_all_data_clear_device_and_network_confirmation">Ê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.</string>
|
||||||
|
<string name="dialog_clear_all_data_clear">Effacer</string>
|
||||||
<string name="dialog_clear_all_data_local_only">Effacer seulement</string>
|
<string name="dialog_clear_all_data_local_only">Effacer seulement</string>
|
||||||
<string name="dialog_clear_all_data_clear_network">Compte complet</string>
|
<string name="dialog_clear_all_data_clear_network">Compte complet</string>
|
||||||
<string name="activity_qr_code_title">Code QR</string>
|
<string name="activity_qr_code_title">QR Code</string>
|
||||||
<string name="activity_qr_code_view_my_qr_code_tab_title">Afficher mon code QR</string>
|
<string name="activity_qr_code_view_my_qr_code_tab_title">Afficher mon QR code</string>
|
||||||
<string name="activity_qr_code_view_scan_qr_code_tab_title">Scanner le code QR</string>
|
<string name="activity_qr_code_view_scan_qr_code_tab_title">Scanner le QR Code</string>
|
||||||
<string name="activity_qr_code_view_scan_qr_code_explanation">Scannez le code QR d\'un autre utilisateur pour démarrer une session</string>
|
<string name="activity_qr_code_view_scan_qr_code_explanation">Scannez le QR code d\'un autre utilisateur pour démarrer une session</string>
|
||||||
<string name="fragment_view_my_qr_code_title">Scannez-moi</string>
|
<string name="fragment_view_my_qr_code_title">Scannez-moi</string>
|
||||||
<string name="fragment_view_my_qr_code_explanation">Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.</string>
|
<string name="fragment_view_my_qr_code_explanation">Ceci est votre QR code. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous.</string>
|
||||||
<string name="fragment_view_my_qr_code_share_title">Partager le code QR</string>
|
<string name="fragment_view_my_qr_code_share_title">Partager le QR code</string>
|
||||||
<string name="fragment_contact_selection_contacts_title">Contacts</string>
|
<string name="fragment_contact_selection_contacts_title">Contacts</string>
|
||||||
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
|
<string name="fragment_contact_selection_closed_groups_title">Groupes privés</string>
|
||||||
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
|
<string name="fragment_contact_selection_open_groups_title">Groupes publics</string>
|
||||||
@ -664,8 +706,8 @@
|
|||||||
<string name="activity_link_device_skip_prompt">Cela prend un certain temps, voulez-vous passer ?</string>
|
<string name="activity_link_device_skip_prompt">Cela prend un certain temps, voulez-vous passer ?</string>
|
||||||
<string name="activity_link_device_link_device">Relier un appareil</string>
|
<string name="activity_link_device_link_device">Relier un appareil</string>
|
||||||
<string name="activity_link_device_recovery_phrase">Phrase de récupération</string>
|
<string name="activity_link_device_recovery_phrase">Phrase de récupération</string>
|
||||||
<string name="activity_link_device_scan_qr_code">Scannez le code QR</string>
|
<string name="activity_link_device_scan_qr_code">Scanner le QR Code</string>
|
||||||
<string name="activity_link_device_qr_message">Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre code QR.</string>
|
<string name="activity_link_device_qr_message">Allez dans Paramètres → Phrase de récupération sur votre autre appareil pour afficher votre QR code.</string>
|
||||||
<string name="activity_join_public_chat_join_rooms">Ou rejoignez l\'un(e) de ceux-ci…</string>
|
<string name="activity_join_public_chat_join_rooms">Ou rejoignez l\'un(e) de ceux-ci…</string>
|
||||||
<string name="activity_pn_mode_message_notifications">Notifications de message</string>
|
<string name="activity_pn_mode_message_notifications">Notifications de message</string>
|
||||||
<string name="activity_pn_mode_explanation">Session peut vous avertir de la présence de nouveaux messages de deux façons.</string>
|
<string name="activity_pn_mode_explanation">Session peut vous avertir de la présence de nouveaux messages de deux façons.</string>
|
||||||
@ -694,6 +736,7 @@
|
|||||||
<string name="dialog_download_explanation">Êtes-vous sûr de vouloir télécharger le média envoyé par %s ?</string>
|
<string name="dialog_download_explanation">Êtes-vous sûr de vouloir télécharger le média envoyé par %s ?</string>
|
||||||
<string name="dialog_download_button_title">Télécharger</string>
|
<string name="dialog_download_button_title">Télécharger</string>
|
||||||
<string name="activity_conversation_blocked_banner_text">%s est bloqué. Débloquer ?</string>
|
<string name="activity_conversation_blocked_banner_text">%s est bloqué. Débloquer ?</string>
|
||||||
|
<string name="activity_conversation_block_user">Bloquer l\'utilisateur</string>
|
||||||
<string name="activity_conversation_attachment_prep_failed">La préparation de la pièce jointe pour l\'envoi a échoué.</string>
|
<string name="activity_conversation_attachment_prep_failed">La préparation de la pièce jointe pour l\'envoi a échoué.</string>
|
||||||
<string name="media">Médias</string>
|
<string name="media">Médias</string>
|
||||||
<string name="UntrustedAttachmentView_download_attachment">Touchez pour télécharger %s</string>
|
<string name="UntrustedAttachmentView_download_attachment">Touchez pour télécharger %s</string>
|
||||||
@ -711,6 +754,116 @@
|
|||||||
<string name="activity_settings_support">Journal de débogage</string>
|
<string name="activity_settings_support">Journal de débogage</string>
|
||||||
<string name="dialog_share_logs_title">Partager les logs</string>
|
<string name="dialog_share_logs_title">Partager les logs</string>
|
||||||
<string name="dialog_share_logs_explanation">Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ?</string>
|
<string name="dialog_share_logs_explanation">Voulez-vous exporter les logs de votre application pour pouvoir partager pour le dépannage ?</string>
|
||||||
<string name="conversation_pin">Code pin</string>
|
<string name="conversation_pin">Épingler</string>
|
||||||
<string name="conversation_unpin">Désépingler</string>
|
<string name="conversation_unpin">Désépingler</string>
|
||||||
|
<string name="mark_all_as_read">Tout marquer comme lu</string>
|
||||||
|
<string name="global_search_contacts_groups">Contacts et Groupes</string>
|
||||||
|
<string name="global_search_messages">Messages</string>
|
||||||
|
<string name="activity_message_requests_title">Demandes de message</string>
|
||||||
|
<string name="message_requests_send_notice">Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message et révélera votre ID de session.</string>
|
||||||
|
<string name="accept">Accepter</string>
|
||||||
|
<string name="decline">Refuser</string>
|
||||||
|
<string name="message_requests_clear_all">Effacer tout</string>
|
||||||
|
<string name="message_requests_decline_message">Êtes-vous sûr de vouloir refuser cette demande de message ?</string>
|
||||||
|
<string name="message_requests_block_message">Êtes-vous sûr de vouloir supprimer cette demande de message ?</string>
|
||||||
|
<string name="message_requests_deleted">Demande de message supprimée</string>
|
||||||
|
<string name="message_requests_clear_all_message">Êtes-vous sûr de vouloir supprimer toutes les demandes de message ?</string>
|
||||||
|
<string name="message_requests_cleared">Demandes de message supprimées</string>
|
||||||
|
<string name="message_requests_accepted">Votre demande de message a été acceptée.</string>
|
||||||
|
<string name="message_requests_pending">Votre demande de message est en attente.</string>
|
||||||
|
<string name="message_request_empty_state_message">Aucune demande de message en attente</string>
|
||||||
|
<string name="NewConversationButton_SessionTooltip">Message privé</string>
|
||||||
|
<string name="NewConversationButton_ClosedGroupTooltip">Groupes privés</string>
|
||||||
|
<string name="NewConversationButton_OpenGroupTooltip">Groupe public</string>
|
||||||
|
<string name="message_requests_notification">Vous avez une nouvelle demande de message</string>
|
||||||
|
<string name="CallNotificationBuilder_connecting">Connexion…</string>
|
||||||
|
<string name="NotificationBarManager__incoming_signal_call">Appel entrant</string>
|
||||||
|
<string name="NotificationBarManager__deny_call">Refuser l’appel</string>
|
||||||
|
<string name="NotificationBarManager__answer_call">Répondre à l’appel</string>
|
||||||
|
<string name="NotificationBarManager_call_in_progress">Appel en cours</string>
|
||||||
|
<string name="NotificationBarManager__cancel_call">Annuler l’appel</string>
|
||||||
|
<string name="NotificationBarManager__establishing_signal_call">Établissement de l\'appel</string>
|
||||||
|
<string name="NotificationBarManager__end_call">Raccrocher</string>
|
||||||
|
<string name="accept_call">Accepter l\'appel</string>
|
||||||
|
<string name="decline_call">Refuser l\'appel</string>
|
||||||
|
<string name="preferences__voice_video_calls">Appels vocaux et vidéos</string>
|
||||||
|
<string name="preferences__calls_beta">Appels (Bêta)</string>
|
||||||
|
<string name="preferences__allow_access_voice_video">Active les appels vocaux et vidéo vers et depuis d\'autres utilisateurs.</string>
|
||||||
|
<string name="dialog_voice_video_title">Appels vocaux / vidéo</string>
|
||||||
|
<string name="dialog_voice_video_message">La version actuelle des appels vocaux/vidéo exposera votre adresse IP aux serveurs de la Fondation Oxen et aux utilisateurs appelés</string>
|
||||||
|
<string name="CallNotificationBuilder_first_call_title">Appel Manqué</string>
|
||||||
|
<string name="CallNotificationBuilder_first_call_message">Vous avez manqué un appel car vous devez activer la permission « Appels vocaux et vidéo » dans les paramètres de confidentialité.</string>
|
||||||
|
<string name="WebRtcCallActivity_Session_Call">Appel Session</string>
|
||||||
|
<string name="WebRtcCallActivity_Reconnecting">Reconnexion…</string>
|
||||||
|
<string name="CallNotificationBuilder_system_notification_title">Notifications</string>
|
||||||
|
<string name="CallNotificationBuilder_system_notification_message">Les notifications désactivées vous empêcheront de recevoir des appels, aller dans les paramètres de notification de session?</string>
|
||||||
|
<string name="dismiss">Rejeter</string>
|
||||||
|
<string name="activity_settings_conversations_button_title">Conversations</string>
|
||||||
|
<string name="activity_settings_message_appearance_button_title">Apparence</string>
|
||||||
|
<string name="activity_settings_help_button">Aide</string>
|
||||||
|
<string name="activity_appearance_themes_category">Thèmes</string>
|
||||||
|
<string name="ocean_dark_theme_name">Océan sombre</string>
|
||||||
|
<string name="classic_dark_theme_name">Sombre classique</string>
|
||||||
|
<string name="ocean_light_theme_name">Océan lumineux</string>
|
||||||
|
<string name="classic_light_theme_name">Clair classique</string>
|
||||||
|
<string name="activity_appearance_primary_color_category">Couleur principale</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__this_message">Ce message</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__recently_used">Fréquemment Utilisés</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people">Émoticônes et personnes</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__nature" comment="Heading for an emoji list's category">Nature</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__food" comment="Heading for an emoji list's category">Nourriture</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__activities">Activités</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__places" comment="Heading for an emoji list's category">Voyage</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__objects">Objets</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__symbols">Symboles</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__flags">Drapeaux</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__emoticons">Emoticônes</string>
|
||||||
|
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__no_results_found">Aucun résultat trouvé</string>
|
||||||
|
<!-- ReactionsBottomSheetDialogFragment -->
|
||||||
|
<string name="ReactionsBottomSheetDialogFragment_all">Tous · %1$d</string>
|
||||||
|
<!-- ReactionsConversationView -->
|
||||||
|
<string name="ReactionsConversationView_plus">+%1$d</string>
|
||||||
|
<!-- ReactionsRecipientAdapter -->
|
||||||
|
<string name="ReactionsRecipientAdapter_you">Vous</string>
|
||||||
|
<string name="reaction_notification">%1$s a réagi à un message %2$s</string>
|
||||||
|
<string name="ReactionsConversationView_show_less">Masquer les détails</string>
|
||||||
|
<string name="KeyboardPagerFragment_search_emoji">Rechercher un émoticône</string>
|
||||||
|
<string name="KeyboardPagerfragment_back_to_emoji">Retour à l\'émoticône</string>
|
||||||
|
<string name="KeyboardPagerfragment_clear_search_entry">Effacer la recherche</string>
|
||||||
|
<string name="activity_appearance_follow_system_category">Thème sombre automatique</string>
|
||||||
|
<string name="activity_appearance_follow_system_explanation">Faire correspondre aux paramètres systèmes</string>
|
||||||
|
<string name="go_to_device_notification_settings">Accédez aux paramètres de notifications de l\'appareil</string>
|
||||||
|
<string name="blocked_contacts_title">Contacts bloqués</string>
|
||||||
|
<string name="blocked_contacts_empty_state">Vous n\'avez aucun contact bloqué</string>
|
||||||
|
<string name="Unblock_dialog__title_single">Débloquer %s</string>
|
||||||
|
<string name="Unblock_dialog__title_multiple">Débloquer les utilisateurs</string>
|
||||||
|
<string name="Unblock_dialog__message">Êtes-vous sûr·e de vouloir débloquer %s ?</string>
|
||||||
|
<plurals name="Unblock_dialog__message_multiple_overflow">
|
||||||
|
<item quantity="one">et %d autre</item>
|
||||||
|
<item quantity="other">et %d autres</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="ReactionsRecipientAdapter_other_reactors">
|
||||||
|
<item quantity="one">Et %1$d autre a réagi %2$s à ce message</item>
|
||||||
|
<item quantity="other">Et %1$d autres ont réagi %2$s à ce message</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="dialog_new_conversation_title">Nouvelle conversation</string>
|
||||||
|
<string name="dialog_new_message_title">Nouveau message</string>
|
||||||
|
<string name="activity_create_group_title">Créer un groupe</string>
|
||||||
|
<string name="dialog_join_community_title">Rejoindre la communauté</string>
|
||||||
|
<string name="new_conversation_contacts_title">Contacts</string>
|
||||||
|
<string name="new_conversation_unknown_contacts_section_title">Inconnu·e</string>
|
||||||
|
<string name="fragment_enter_public_key_prompt">Commencez une nouvelle conversation en entrant l\'ID Session de quelqu\'un ou en lui partageant votre ID Session.</string>
|
||||||
|
<string name="activity_create_group_create_button_title">Créer</string>
|
||||||
|
<string name="search_contacts_hint">Rechercher parmi les contacts</string>
|
||||||
|
<string name="activity_join_public_chat_enter_community_url_tab_title">URL de la communauté</string>
|
||||||
|
<string name="fragment_enter_community_url_edit_text_hint">Entrez l\'URL de la communauté</string>
|
||||||
|
<string name="fragment_enter_community_url_join_button_title">Rejoindre</string>
|
||||||
|
<string name="new_conversation_dialog_back_button_content_description">Revenir en arrière</string>
|
||||||
|
<string name="new_conversation_dialog_close_button_content_description">Fermer la fenêtre</string>
|
||||||
|
<string name="ErrorNotifier_migration">Échec de la mise à jour de la base de données</string>
|
||||||
|
<string name="ErrorNotifier_migration_downgrade">Veuillez contacter le support pour signaler l\'erreur.</string>
|
||||||
|
<string name="delivery_status_sending">Envoi</string>
|
||||||
|
<string name="delivery_status_read">Lu</string>
|
||||||
|
<string name="delivery_status_sent">Envoyé</string>
|
||||||
|
<string name="delivery_status_failed">Échec d’envoi</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -77,6 +77,7 @@
|
|||||||
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
|
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
|
||||||
<string name="ConversationActivity_unable_to_record_audio">Unable to record audio!</string>
|
<string name="ConversationActivity_unable_to_record_audio">Unable to record audio!</string>
|
||||||
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string>
|
<string name="ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device">There is no app available to handle this link on your device.</string>
|
||||||
|
<string name="ConversationActivity_copy_open_group_url">Copy Community URL</string>
|
||||||
<string name="ConversationActivity_invite_to_open_group">Add members</string>
|
<string name="ConversationActivity_invite_to_open_group">Add members</string>
|
||||||
<string name="ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone">Session needs microphone access to send audio messages.</string>
|
<string name="ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone">Session needs microphone access to send audio messages.</string>
|
||||||
<string name="ConversationActivity_signal_requires_the_microphone_permission_in_order_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\".</string>
|
<string name="ConversationActivity_signal_requires_the_microphone_permission_in_order_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\".</string>
|
||||||
@ -870,4 +871,6 @@
|
|||||||
<string name="delivery_status_read">Read</string>
|
<string name="delivery_status_read">Read</string>
|
||||||
<string name="delivery_status_sent">Sent</string>
|
<string name="delivery_status_sent">Sent</string>
|
||||||
<string name="delivery_status_failed">Failed to send</string>
|
<string name="delivery_status_failed">Failed to send</string>
|
||||||
|
<string name="giphy_permission_title">Search GIFs?</string>
|
||||||
|
<string name="giphy_permission_message">Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -366,7 +366,7 @@
|
|||||||
<item name="conversationMenuSearchBackgroundColor">@color/classic_dark_0</item>
|
<item name="conversationMenuSearchBackgroundColor">@color/classic_dark_0</item>
|
||||||
|
|
||||||
<!-- Conversation -->
|
<!-- Conversation -->
|
||||||
<item name="message_received_background_color">@color/classic_dark_2</item>
|
<item name="message_received_background_color">@color/classic_dark_3</item>
|
||||||
<item name="message_received_text_color">@color/classic_dark_6</item>
|
<item name="message_received_text_color">@color/classic_dark_6</item>
|
||||||
<item name="message_sent_background_color">?colorAccent</item>
|
<item name="message_sent_background_color">?colorAccent</item>
|
||||||
<item name="message_sent_text_color">@color/classic_dark_0</item>
|
<item name="message_sent_text_color">@color/classic_dark_0</item>
|
||||||
@ -386,7 +386,7 @@
|
|||||||
<item name="scroll_to_bottom_button_background">@color/classic_dark_1</item>
|
<item name="scroll_to_bottom_button_background">@color/classic_dark_1</item>
|
||||||
<item name="scroll_to_bottom_button_border">@color/classic_dark_3</item>
|
<item name="scroll_to_bottom_button_border">@color/classic_dark_3</item>
|
||||||
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
|
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
|
||||||
<item name="message_selected">@color/classic_dark_1</item>
|
<item name="message_selected">@color/classic_dark_2</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Classic.Light">
|
<style name="Classic.Light">
|
||||||
@ -451,7 +451,7 @@
|
|||||||
<item name="conversationMenuSearchBackgroundColor">@color/classic_light_6</item>
|
<item name="conversationMenuSearchBackgroundColor">@color/classic_light_6</item>
|
||||||
|
|
||||||
<!-- Conversation -->
|
<!-- Conversation -->
|
||||||
<item name="message_received_background_color">@color/classic_light_4</item>
|
<item name="message_received_background_color">@color/classic_light_3</item>
|
||||||
<item name="message_received_text_color">@color/classic_light_0</item>
|
<item name="message_received_text_color">@color/classic_light_0</item>
|
||||||
<item name="message_sent_background_color">?colorAccent</item>
|
<item name="message_sent_background_color">?colorAccent</item>
|
||||||
<item name="message_sent_text_color">@color/classic_light_0</item>
|
<item name="message_sent_text_color">@color/classic_light_0</item>
|
||||||
@ -471,7 +471,7 @@
|
|||||||
<item name="scroll_to_bottom_button_background">@color/classic_light_4</item>
|
<item name="scroll_to_bottom_button_background">@color/classic_light_4</item>
|
||||||
<item name="scroll_to_bottom_button_border">@color/classic_light_2</item>
|
<item name="scroll_to_bottom_button_border">@color/classic_light_2</item>
|
||||||
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
|
<item name="conversation_unread_count_indicator_background">@color/classic_dark_4</item>
|
||||||
<item name="message_selected">@color/classic_light_5</item>
|
<item name="message_selected">@color/classic_light_4</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@ -640,7 +640,7 @@
|
|||||||
<item name="conversation_pinned_background_color">?colorCellBackground</item>
|
<item name="conversation_pinned_background_color">?colorCellBackground</item>
|
||||||
<item name="conversation_unread_background_color">@color/ocean_light_6</item>
|
<item name="conversation_unread_background_color">@color/ocean_light_6</item>
|
||||||
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
|
<item name="conversation_pinned_icon_color">?android:textColorSecondary</item>
|
||||||
<item name="message_selected">@color/ocean_light_6</item>
|
<item name="message_selected">@color/ocean_light_5</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -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<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
|
||||||
on { address } doReturn Address.fromSerialized(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP)
|
|
||||||
on { isOpenGroupRecipient } doReturn true
|
|
||||||
additionalMocks?.let { it(this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newOpenGroupRecipient(additionalMocks: ((KStubbing<Recipient>) -> Unit) ? = null) = mock<Recipient> {
|
|
||||||
on { address } doReturn Address.fromSerialized(EXAMPLE_NEW_ENCODED_OPEN_GROUP)
|
|
||||||
on { isOpenGroupRecipient } doReturn true
|
|
||||||
additionalMocks?.let { it(this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun legacyThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit) ? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
|
||||||
val returnedRecipient = legacyOpenGroupRecipient(additionalRecipientMocks)
|
|
||||||
on { recipient } doReturn returnedRecipient
|
|
||||||
on { threadId } doReturn LEGACY_THREAD_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun newThreadRecord(additionalRecipientMocks: ((KStubbing<Recipient>) -> Unit)? = null, additionalThreadMocks: ((KStubbing<ThreadRecord>) -> Unit)? = null) = mock<ThreadRecord> {
|
|
||||||
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<ThreadDatabase> {
|
|
||||||
on { legacyOxenOpenGroups } doReturn emptyList()
|
|
||||||
}
|
|
||||||
val mockedDbComponent = mock<DatabaseComponent> {
|
|
||||||
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<Long>()
|
|
||||||
val capturedNewEncoded = argumentCaptor<String>()
|
|
||||||
val mockedThreadDb = mock<ThreadDatabase> {
|
|
||||||
val legacyThreadRecord = legacyThreadRecord()
|
|
||||||
on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord)
|
|
||||||
on { httpsOxenOpenGroups } doReturn emptyList()
|
|
||||||
on { migrateEncodedGroup(capturedThreadId.capture(), capturedNewEncoded.capture()) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock groupDB
|
|
||||||
val capturedGroupLegacyEncoded = argumentCaptor<String>()
|
|
||||||
val capturedGroupNewEncoded = argumentCaptor<String>()
|
|
||||||
val mockedGroupDb = mock<GroupDatabase> {
|
|
||||||
on {
|
|
||||||
migrateEncodedGroup(
|
|
||||||
capturedGroupLegacyEncoded.capture(),
|
|
||||||
capturedGroupNewEncoded.capture()
|
|
||||||
)
|
|
||||||
} doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock LokiAPIDB
|
|
||||||
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
|
||||||
val capturedLokiNewGroup = argumentCaptor<String>()
|
|
||||||
val mockedLokiApi = mock<LokiAPIDatabase> {
|
|
||||||
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<OpenGroup>()
|
|
||||||
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
|
||||||
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<DatabaseComponent> {
|
|
||||||
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<Long>()
|
|
||||||
val mockedThreadDb = mock<ThreadDatabase> {
|
|
||||||
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<String>()
|
|
||||||
val mockedGroupDb = mock<GroupDatabase> {
|
|
||||||
on { delete(capturedGroupLegacyEncoded.capture()) } doReturn true
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock LokiAPIDB
|
|
||||||
val capturedLokiLegacyGroup = argumentCaptor<String>()
|
|
||||||
val capturedLokiNewGroup = argumentCaptor<String>()
|
|
||||||
val mockedLokiApi = mock<LokiAPIDatabase> {
|
|
||||||
on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock messaging dbs
|
|
||||||
val migrateMmsFromThreadId = argumentCaptor<Long>()
|
|
||||||
val migrateMmsToThreadId = argumentCaptor<Long>()
|
|
||||||
|
|
||||||
val mockedMmsDb = mock<MmsDatabase> {
|
|
||||||
on { migrateThreadId(migrateMmsFromThreadId.capture(), migrateMmsToThreadId.capture()) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val migrateSmsFromThreadId = argumentCaptor<Long>()
|
|
||||||
val migrateSmsToThreadId = argumentCaptor<Long>()
|
|
||||||
val mockedSmsDb = mock<SmsDatabase> {
|
|
||||||
on { migrateThreadId(migrateSmsFromThreadId.capture(), migrateSmsToThreadId.capture()) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val lokiFromThreadId = argumentCaptor<Long>()
|
|
||||||
val lokiToThreadId = argumentCaptor<Long>()
|
|
||||||
val mockedLokiMessageDatabase = mock<LokiMessageDatabase> {
|
|
||||||
on { migrateThreadId(lokiFromThreadId.capture(), lokiToThreadId.capture()) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mockedLokiThreadDb = mock<LokiThreadDatabase> {
|
|
||||||
on { removeOpenGroupChat(eq(LEGACY_THREAD_ID)) } doAnswer {}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mockedDbComponent = mock<DatabaseComponent> {
|
|
||||||
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)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -52,7 +52,7 @@ interface StorageProtocol {
|
|||||||
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
|
fun getAttachmentUploadJob(attachmentID: Long): AttachmentUploadJob?
|
||||||
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
fun getMessageSendJob(messageSendJobID: String): MessageSendJob?
|
||||||
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
|
fun getMessageReceiveJob(messageReceiveJobID: String): Job?
|
||||||
fun getGroupAvatarDownloadJob(server: String, room: String): Job?
|
fun getGroupAvatarDownloadJob(server: String, room: String, imageId: String?): Job?
|
||||||
fun getConfigSyncJob(destination: Destination): Job?
|
fun getConfigSyncJob(destination: Destination): Job?
|
||||||
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
|
||||||
fun isJobCanceled(job: Job): Boolean
|
fun isJobCanceled(job: Job): Boolean
|
||||||
@ -84,6 +84,7 @@ interface StorageProtocol {
|
|||||||
// Open Group Metadata
|
// Open Group Metadata
|
||||||
fun updateTitle(groupID: String, newValue: String)
|
fun updateTitle(groupID: String, newValue: String)
|
||||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||||
|
fun removeProfilePicture(groupID: String)
|
||||||
fun hasDownloadedProfilePicture(groupID: String): Boolean
|
fun hasDownloadedProfilePicture(groupID: String): Boolean
|
||||||
fun setUserCount(room: String, server: String, newValue: Int)
|
fun setUserCount(room: String, server: String, newValue: Int)
|
||||||
|
|
||||||
|
@ -16,15 +16,6 @@ object FileServerApi {
|
|||||||
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||||
const val server = "http://filev2.getsession.org"
|
const val server = "http://filev2.getsession.org"
|
||||||
const val maxFileSize = 10_000_000 // 10 MB
|
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) {
|
sealed class Error(message: String) : Exception(message) {
|
||||||
object ParsingFailed : Error("Invalid response.")
|
object ParsingFailed : Error("Invalid response.")
|
||||||
|
@ -42,7 +42,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
|
private val TS_INCOMING_MESSAGE_ID_KEY = "tsIncoming_message_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val threadID = storage.getThreadIdForMms(databaseMessageID)
|
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")
|
Log.d("AttachmentDownloadJob", "Setting attachment state = failed, don't have attachment")
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
|
||||||
}
|
}
|
||||||
this.handlePermanentFailure(exception)
|
this.handlePermanentFailure(dispatcherName, exception)
|
||||||
} else if (exception == Error.DuplicateData) {
|
} else if (exception == Error.DuplicateData) {
|
||||||
attachment?.let { id ->
|
attachment?.let { id ->
|
||||||
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
|
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")
|
Log.d("AttachmentDownloadJob", "Setting attachment state = done from duplicate data")
|
||||||
messageDataProvider.setAttachmentState(AttachmentState.DONE, AttachmentId(attachmentID,0), databaseMessageID)
|
messageDataProvider.setAttachmentState(AttachmentState.DONE, AttachmentId(attachmentID,0), databaseMessageID)
|
||||||
}
|
}
|
||||||
this.handleSuccess()
|
this.handleSuccess(dispatcherName)
|
||||||
} else {
|
} else {
|
||||||
if (failureCount + 1 >= maxFailureCount) {
|
if (failureCount + 1 >= maxFailureCount) {
|
||||||
attachment?.let { id ->
|
attachment?.let { id ->
|
||||||
@ -79,7 +79,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
|
|||||||
messageDataProvider.setAttachmentState(AttachmentState.FAILED, AttachmentId(attachmentID,0), databaseMessageID)
|
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")
|
Log.d("AttachmentDownloadJob", "deleting tempfile")
|
||||||
tempFile.delete()
|
tempFile.delete()
|
||||||
Log.d("AttachmentDownloadJob", "succeeding job")
|
Log.d("AttachmentDownloadJob", "succeeding job")
|
||||||
handleSuccess()
|
handleSuccess(dispatcherName)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("AttachmentDownloadJob", "Error processing attachment download", e)
|
Log.e("AttachmentDownloadJob", "Error processing attachment download", e)
|
||||||
tempFile?.delete()
|
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.")
|
Log.w("AttachmentDownloadJob", "Attachment downloaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermanentFailure(e: Exception) {
|
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
|
||||||
delegate?.handleJobFailedPermanently(this, e)
|
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(e: Exception) {
|
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTempFile(): File {
|
private fun createTempFile(): File {
|
||||||
|
@ -49,29 +49,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
|
private val MESSAGE_SEND_JOB_ID_KEY = "message_send_job_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
try {
|
try {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
|
||||||
?: return handleFailure(Error.NoAttachment)
|
?: return handleFailure(dispatcherName, Error.NoAttachment)
|
||||||
val openGroup = storage.getOpenGroup(threadID.toLong())
|
val openGroup = storage.getOpenGroup(threadID.toLong())
|
||||||
if (openGroup != null) {
|
if (openGroup != null) {
|
||||||
val keyAndResult = upload(attachment, openGroup.server, false) {
|
val keyAndResult = upload(attachment, openGroup.server, false) {
|
||||||
OpenGroupApi.upload(it, openGroup.room, openGroup.server)
|
OpenGroupApi.upload(it, openGroup.room, openGroup.server)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
|
||||||
} else {
|
} else {
|
||||||
val keyAndResult = upload(attachment, FileServerApi.server, true) {
|
val keyAndResult = upload(attachment, FileServerApi.server, true) {
|
||||||
FileServerApi.upload(it)
|
FileServerApi.upload(it)
|
||||||
}
|
}
|
||||||
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
|
handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second)
|
||||||
}
|
}
|
||||||
} catch (e: java.lang.Exception) {
|
} catch (e: java.lang.Exception) {
|
||||||
if (e == Error.NoAttachment) {
|
if (e == Error.NoAttachment) {
|
||||||
this.handlePermanentFailure(e)
|
this.handlePermanentFailure(dispatcherName, e)
|
||||||
} else {
|
} else {
|
||||||
this.handleFailure(e)
|
this.handleFailure(dispatcherName, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,9 +108,9 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
return Pair(key, UploadResult(id, "${server}/file/$id", digest))
|
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.")
|
Log.d(TAG, "Attachment uploaded successfully.")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult)
|
||||||
if (attachment.contentType.startsWith("audio/")) {
|
if (attachment.contentType.startsWith("audio/")) {
|
||||||
@ -148,16 +148,16 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
|
|||||||
storage.resumeMessageSendJobIfNeeded(messageSendJobID)
|
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.")
|
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)
|
MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID)
|
||||||
failAssociatedMessageSendJob(e)
|
failAssociatedMessageSendJob(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(e: Exception) {
|
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||||
Log.w(TAG, "Attachment upload failed due to error: $this.")
|
Log.w(TAG, "Attachment upload failed due to error: $this.")
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
if (failureCount + 1 >= maxFailureCount) {
|
if (failureCount + 1 >= maxFailureCount) {
|
||||||
failAssociatedMessageSendJob(e)
|
failAssociatedMessageSendJob(e)
|
||||||
}
|
}
|
||||||
|
@ -27,32 +27,32 @@ class BackgroundGroupAddJob(val joinUrl: String): Job {
|
|||||||
return "$server.$room"
|
return "$server.$room"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
try {
|
try {
|
||||||
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
|
val openGroup = OpenGroupUrlParser.parseUrl(joinUrl)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL }
|
||||||
if (allOpenGroups.contains(openGroup.joinUrl())) {
|
if (allOpenGroups.contains(openGroup.joinUrl())) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException())
|
||||||
delegate?.handleJobFailed(this, DuplicateGroupException())
|
delegate?.handleJobFailed(this, dispatcherName, DuplicateGroupException())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get image
|
// get image
|
||||||
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey)
|
||||||
val info = storage.addOpenGroup(openGroup.joinUrl())
|
val info = storage.addOpenGroup(openGroup.joinUrl())
|
||||||
val imageId = info?.imageId
|
val imageId = info?.imageId
|
||||||
if (imageId != null) {
|
if (imageId != null && storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, imageId) == null) {
|
||||||
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.room, openGroup.server))
|
JobQueue.shared.add(GroupAvatarDownloadJob(openGroup.server, openGroup.room, imageId))
|
||||||
}
|
}
|
||||||
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
Log.d(KEY, "onOpenGroupAdded(${openGroup.server})")
|
||||||
storage.onOpenGroupAdded(openGroup.server)
|
storage.onOpenGroupAdded(openGroup.server)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
Log.e("OpenGroupDispatcher", "Failed to add group because",e)
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Log.d("Loki", "Group added successfully")
|
Log.d("Loki", "Group added successfully")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data = Data.Builder()
|
override fun serialize(): Data = Data.Builder()
|
||||||
|
@ -70,11 +70,11 @@ class BatchMessageReceiveJob(
|
|||||||
return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID)
|
return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
executeAsync().get()
|
executeAsync(dispatcherName).get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeAsync(): Promise<Unit, Exception> {
|
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
|
||||||
return task {
|
return task {
|
||||||
val threadMap = mutableMapOf<Long, MutableList<ParsedMessage>>()
|
val threadMap = mutableMapOf<Long, MutableList<ParsedMessage>>()
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
@ -185,19 +185,21 @@ class BatchMessageReceiveJob(
|
|||||||
deferredThreadMap.awaitAll()
|
deferredThreadMap.awaitAll()
|
||||||
}
|
}
|
||||||
if (failures.isEmpty()) {
|
if (failures.isEmpty()) {
|
||||||
handleSuccess()
|
handleSuccess(dispatcherName)
|
||||||
} else {
|
} else {
|
||||||
handleFailure()
|
handleFailure(dispatcherName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess() {
|
private fun handleSuccess(dispatcherName: String) {
|
||||||
this.delegate?.handleJobSucceeded(this)
|
Log.i(TAG, "Completed processing of ${messages.size} messages")
|
||||||
|
this.delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure() {
|
private fun handleFailure(dispatcherName: String) {
|
||||||
this.delegate?.handleJobFailed(this, Exception("One or more jobs resulted in failure"))
|
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 {
|
override fun serialize(): Data {
|
||||||
|
@ -5,24 +5,43 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
|
|||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsession.utilities.GroupUtil
|
import org.session.libsession.utilities.GroupUtil
|
||||||
|
|
||||||
class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
class GroupAvatarDownloadJob(val server: String, val room: String, val imageId: String?) : Job {
|
||||||
|
|
||||||
override var delegate: JobDelegate? = null
|
override var delegate: JobDelegate? = null
|
||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
override val maxFailureCount: Int = 10
|
override val maxFailureCount: Int = 10
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
|
if (imageId == null) {
|
||||||
|
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob now requires imageId"))
|
||||||
|
return
|
||||||
|
}
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val imageId = storage.getOpenGroup(room, server)?.imageId ?: return
|
val storedImageId = storage.getOpenGroup(room, server)?.imageId
|
||||||
|
|
||||||
|
if (storedImageId == null || storedImageId != imageId) {
|
||||||
|
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId does not match the OpenGroup"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
|
val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(server, room, imageId).get()
|
||||||
|
|
||||||
|
// Once the download is complete the imageId might no longer match, so we need to fetch it again just in case
|
||||||
|
val postDownloadStoredImageId = storage.getOpenGroup(room, server)?.imageId
|
||||||
|
|
||||||
|
if (postDownloadStoredImageId == null || postDownloadStoredImageId != imageId) {
|
||||||
|
delegate?.handleJobFailedPermanently(this, dispatcherName, Exception("GroupAvatarDownloadJob imageId no longer matches the OpenGroup"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
|
||||||
storage.updateProfilePicture(groupId, bytes)
|
storage.updateProfilePicture(groupId, bytes)
|
||||||
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
storage.updateTimestampUpdated(groupId, System.currentTimeMillis())
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +49,7 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
|||||||
return Data.Builder()
|
return Data.Builder()
|
||||||
.putString(ROOM, room)
|
.putString(ROOM, room)
|
||||||
.putString(SERVER, server)
|
.putString(SERVER, server)
|
||||||
|
.putString(IMAGE_ID, imageId)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,14 +60,16 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
|
|||||||
|
|
||||||
private const val ROOM = "room"
|
private const val ROOM = "room"
|
||||||
private const val SERVER = "server"
|
private const val SERVER = "server"
|
||||||
|
private const val IMAGE_ID = "imageId"
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory : Job.Factory<GroupAvatarDownloadJob> {
|
class Factory : Job.Factory<GroupAvatarDownloadJob> {
|
||||||
|
|
||||||
override fun create(data: Data): GroupAvatarDownloadJob {
|
override fun create(data: Data): GroupAvatarDownloadJob {
|
||||||
return GroupAvatarDownloadJob(
|
return GroupAvatarDownloadJob(
|
||||||
|
data.getString(SERVER),
|
||||||
data.getString(ROOM),
|
data.getString(ROOM),
|
||||||
data.getString(SERVER)
|
if (data.hasString(IMAGE_ID)) { data.getString(IMAGE_ID) } else { null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ interface Job {
|
|||||||
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
internal const val MAX_BUFFER_SIZE = 1_000_000 // bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun execute()
|
suspend fun execute(dispatcherName: String)
|
||||||
|
|
||||||
fun serialize(): Data
|
fun serialize(): Data
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ package org.session.libsession.messaging.jobs
|
|||||||
|
|
||||||
interface JobDelegate {
|
interface JobDelegate {
|
||||||
|
|
||||||
fun handleJobSucceeded(job: Job)
|
fun handleJobSucceeded(job: Job, dispatcherName: String)
|
||||||
fun handleJobFailed(job: Job, error: Exception)
|
fun handleJobFailed(job: Job, dispatcherName: String, error: Exception)
|
||||||
fun handleJobFailedPermanently(job: Job, error: Exception)
|
fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception)
|
||||||
}
|
}
|
@ -53,7 +53,7 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
if (openGroupId.isNullOrEmpty()) {
|
if (openGroupId.isNullOrEmpty()) {
|
||||||
Log.e("OpenGroupDispatcher", "Open Group ID was null on ${job.javaClass.simpleName}")
|
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 {
|
} else {
|
||||||
val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) {
|
val groupChannel = if (!openGroupChannels.containsKey(openGroupId)) {
|
||||||
Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel")
|
Log.d("OpenGroupDispatcher", "Creating ${openGroupId.hashCode()} channel")
|
||||||
@ -95,9 +95,16 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun Job.process(dispatcherName: String) {
|
private suspend fun Job.process(dispatcherName: String) {
|
||||||
Log.d(dispatcherName,"processJob: ${javaClass.simpleName}")
|
Log.d(dispatcherName,"processJob: ${javaClass.simpleName} (id: $id)")
|
||||||
delegate = this@JobQueue
|
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 {
|
init {
|
||||||
@ -177,7 +184,7 @@ class JobQueue : JobDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!pendingJobIds.add(id)) {
|
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
|
return
|
||||||
}
|
}
|
||||||
queue.trySend(job)
|
queue.trySend(job)
|
||||||
@ -196,7 +203,7 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingJobs.sortedBy { it.id }.forEach { job ->
|
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
|
queue.trySend(job) // Offer always called on unlimited capacity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,21 +231,21 @@ class JobQueue : JobDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobSucceeded(job: Job) {
|
override fun handleJobSucceeded(job: Job, dispatcherName: String) {
|
||||||
val jobId = job.id ?: return
|
val jobId = job.id ?: return
|
||||||
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
MessagingModuleConfiguration.shared.storage.markJobAsSucceeded(jobId)
|
||||||
pendingJobIds.remove(jobId)
|
pendingJobIds.remove(jobId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobFailed(job: Job, error: Exception) {
|
override fun handleJobFailed(job: Job, dispatcherName: String, error: Exception) {
|
||||||
// Canceled
|
// Canceled
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
if (storage.isJobCanceled(job)) {
|
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
|
// Message send jobs waiting for the attachment to upload
|
||||||
if (job is MessageSendJob && error is MessageSendJob.AwaitingAttachmentUploadException) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,21 +263,22 @@ class JobQueue : JobDelegate {
|
|||||||
job.failureCount += 1
|
job.failureCount += 1
|
||||||
|
|
||||||
if (job.failureCount >= job.maxFailureCount) {
|
if (job.failureCount >= job.maxFailureCount) {
|
||||||
handleJobFailedPermanently(job, error)
|
handleJobFailedPermanently(job, dispatcherName, error)
|
||||||
} else {
|
} else {
|
||||||
storage.persistJob(job)
|
storage.persistJob(job)
|
||||||
val retryInterval = getRetryInterval(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) {
|
timer.schedule(delay = retryInterval) {
|
||||||
Log.i("Loki", "Retrying ${job::class.simpleName}.")
|
Log.i("Loki", "Retrying ${job::class.simpleName} (id: ${job.id}).")
|
||||||
queue.trySend(job)
|
queue.trySend(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleJobFailedPermanently(job: Job, error: Exception) {
|
override fun handleJobFailedPermanently(job: Job, dispatcherName: String, error: Exception) {
|
||||||
val jobId = job.id ?: return
|
val jobId = job.id ?: return
|
||||||
handleJobFailedPermanently(jobId)
|
handleJobFailedPermanently(jobId)
|
||||||
|
Log.d(dispatcherName, "permanentlyFailedJob: ${javaClass.simpleName} (id: ${job.id})")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleJobFailedPermanently(jobId: String) {
|
private fun handleJobFailedPermanently(jobId: String) {
|
||||||
|
@ -25,11 +25,11 @@ class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val
|
|||||||
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
executeAsync().get()
|
executeAsync(dispatcherName).get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun executeAsync(): Promise<Unit, Exception> {
|
fun executeAsync(dispatcherName: String): Promise<Unit, Exception> {
|
||||||
val deferred = deferred<Unit, Exception>()
|
val deferred = deferred<Unit, Exception>()
|
||||||
try {
|
try {
|
||||||
val isRetry: Boolean = failureCount != 0
|
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)
|
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID, openGroupPublicKey = serverPublicKey)
|
||||||
message.serverHash = serverHash
|
message.serverHash = serverHash
|
||||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||||
this.handleSuccess()
|
this.handleSuccess(dispatcherName)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Couldn't receive message.", e)
|
Log.e(TAG, "Couldn't receive message.", e)
|
||||||
if (e is MessageReceiver.Error && !e.isRetryable) {
|
if (e is MessageReceiver.Error && !e.isRetryable) {
|
||||||
Log.e("Loki", "Message receive job permanently failed.", e)
|
Log.e("Loki", "Message receive job permanently failed.", e)
|
||||||
this.handlePermanentFailure(e)
|
this.handlePermanentFailure(dispatcherName, e)
|
||||||
} else {
|
} else {
|
||||||
Log.e("Loki", "Couldn't receive message.", e)
|
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
|
deferred.resolve(Unit) // The promise is just used to keep track of when we're done
|
||||||
}
|
}
|
||||||
return deferred.promise
|
return deferred.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess() {
|
private fun handleSuccess(dispatcherName: String) {
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermanentFailure(e: Exception) {
|
private fun handlePermanentFailure(dispatcherName: String, e: Exception) {
|
||||||
delegate?.handleJobFailedPermanently(this, e)
|
delegate?.handleJobFailedPermanently(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(e: Exception) {
|
private fun handleFailure(dispatcherName: String, e: Exception) {
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
|
@ -32,7 +32,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
private val DESTINATION_KEY = "destination"
|
private val DESTINATION_KEY = "destination"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val message = message as? VisibleMessage
|
val message = message as? VisibleMessage
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
@ -60,12 +60,12 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attachmentsToUpload.isNotEmpty()) {
|
if (attachmentsToUpload.isNotEmpty()) {
|
||||||
this.handleFailure(AwaitingAttachmentUploadException)
|
this.handleFailure(dispatcherName, AwaitingAttachmentUploadException)
|
||||||
return
|
return
|
||||||
} // Wait for all attachments to upload before continuing
|
} // Wait for all attachments to upload before continuing
|
||||||
}
|
}
|
||||||
val promise = MessageSender.send(this.message, this.destination).success {
|
val promise = MessageSender.send(this.message, this.destination).success {
|
||||||
this.handleSuccess()
|
this.handleSuccess(dispatcherName)
|
||||||
}.fail { exception ->
|
}.fail { exception ->
|
||||||
var logStacktrace = true
|
var logStacktrace = true
|
||||||
|
|
||||||
@ -74,14 +74,14 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
is HTTP.HTTPRequestFailedException -> {
|
is HTTP.HTTPRequestFailedException -> {
|
||||||
logStacktrace = false
|
logStacktrace = false
|
||||||
|
|
||||||
if (exception.statusCode == 429) { this.handlePermanentFailure(exception) }
|
if (exception.statusCode == 429) { this.handlePermanentFailure(dispatcherName, exception) }
|
||||||
else { this.handleFailure(exception) }
|
else { this.handleFailure(dispatcherName, exception) }
|
||||||
}
|
}
|
||||||
is MessageSender.Error -> {
|
is MessageSender.Error -> {
|
||||||
if (!exception.isRetryable) { this.handlePermanentFailure(exception) }
|
if (!exception.isRetryable) { this.handlePermanentFailure(dispatcherName, exception) }
|
||||||
else { this.handleFailure(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) }
|
if (logStacktrace) { Log.e(TAG, "Couldn't send message due to error", exception) }
|
||||||
@ -94,15 +94,15 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess() {
|
private fun handleSuccess(dispatcherName: String) {
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePermanentFailure(error: Exception) {
|
private fun handlePermanentFailure(dispatcherName: String, error: Exception) {
|
||||||
delegate?.handleJobFailedPermanently(this, error)
|
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.")
|
Log.w(TAG, "Failed to send $message::class.simpleName.")
|
||||||
val message = message as? VisibleMessage
|
val message = message as? VisibleMessage
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
@ -110,7 +110,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
|
|||||||
return // The message has been deleted
|
return // The message has been deleted
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate?.handleJobFailed(this, error)
|
delegate?.handleJobFailed(this, dispatcherName, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
|
@ -30,7 +30,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
private val MESSAGE_KEY = "message"
|
private val MESSAGE_KEY = "message"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val server = PushNotificationAPI.server
|
val server = PushNotificationAPI.server
|
||||||
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
val parameters = mapOf( "data" to message.data, "send_to" to message.recipient )
|
||||||
val url = "${server}/notify"
|
val url = "${server}/notify"
|
||||||
@ -46,18 +46,18 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
|
|||||||
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
Log.d("Loki", "Couldn't notify PN server due to error: $exception.")
|
||||||
}
|
}
|
||||||
}.success {
|
}.success {
|
||||||
handleSuccess()
|
handleSuccess(dispatcherName)
|
||||||
}. fail {
|
}. fail {
|
||||||
handleFailure(it)
|
handleFailure(dispatcherName, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSuccess() {
|
private fun handleSuccess(dispatcherName: String) {
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFailure(error: Exception) {
|
private fun handleFailure(dispatcherName: String, error: Exception) {
|
||||||
delegate?.handleJobFailed(this, error)
|
delegate?.handleJobFailed(this, dispatcherName, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
|
@ -19,7 +19,7 @@ class OpenGroupDeleteJob(private val messageServerIds: LongArray, private val th
|
|||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
override val maxFailureCount: Int = 1
|
override val maxFailureCount: Int = 1
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val numberToDelete = messageServerIds.size
|
val numberToDelete = messageServerIds.size
|
||||||
Log.d(TAG, "Deleting $numberToDelete messages")
|
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")
|
Log.d(TAG, "Deleted ${messageIds.first.size + messageIds.second.size} messages successfully")
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
catch (e: Exception) {
|
catch (e: Exception) {
|
||||||
delegate?.handleJobFailed(this, e)
|
delegate?.handleJobFailed(this, dispatcherName, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class TrimThreadJob(val threadId: Long, val openGroupId: String?) : Job {
|
|||||||
const val THREAD_LENGTH_TRIGGER_SIZE = 2000
|
const val THREAD_LENGTH_TRIGGER_SIZE = 2000
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute() {
|
override suspend fun execute(dispatcherName: String) {
|
||||||
val context = MessagingModuleConfiguration.shared.context
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context)
|
val trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context)
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
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
|
val oldestMessageTime = System.currentTimeMillis() - TRIM_TIME_LIMIT
|
||||||
storage.trimThreadBefore(threadId, oldestMessageTime)
|
storage.trimThreadBefore(threadId, oldestMessageTime)
|
||||||
}
|
}
|
||||||
delegate?.handleJobSucceeded(this)
|
delegate?.handleJobSucceeded(this, dispatcherName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
|
@ -36,7 +36,7 @@ data class OpenGroup(
|
|||||||
val server = json.get("server").asText().lowercase(Locale.US)
|
val server = json.get("server").asText().lowercase(Locale.US)
|
||||||
val displayName = json.get("displayName").asText()
|
val displayName = json.get("displayName").asText()
|
||||||
val publicKey = json.get("publicKey").asText()
|
val publicKey = json.get("publicKey").asText()
|
||||||
val imageId = json.get("imageId")?.asText()
|
val imageId = if (json.hasNonNull("imageId")) { json.get("imageId")?.asText() } else { null }
|
||||||
val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true
|
val canWrite = json.get("canWrite")?.asText()?.toBoolean() ?: true
|
||||||
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
|
||||||
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates)
|
OpenGroup(server = server, room = room, name = displayName, publicKey = publicKey, imageId = imageId, canWrite = canWrite, infoUpdates = infoUpdates)
|
||||||
|
@ -159,20 +159,30 @@ class OpenGroupPoller(private val server: String, private val executorService: S
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the group avatar
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
pollInfo.details != null &&
|
pollInfo.details != null &&
|
||||||
pollInfo.details.imageId != null && (
|
pollInfo.details.imageId != null && (
|
||||||
pollInfo.details.imageId != existingOpenGroup.imageId ||
|
pollInfo.details.imageId != existingOpenGroup.imageId ||
|
||||||
!storage.hasDownloadedProfilePicture(dbGroupId)
|
!storage.hasDownloadedProfilePicture(dbGroupId)
|
||||||
)
|
) &&
|
||||||
|
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, pollInfo.details.imageId) == null
|
||||||
) || (
|
) || (
|
||||||
pollInfo.details == null &&
|
pollInfo.details == null &&
|
||||||
existingOpenGroup.imageId != null &&
|
existingOpenGroup.imageId != null &&
|
||||||
!storage.hasDownloadedProfilePicture(dbGroupId)
|
!storage.hasDownloadedProfilePicture(dbGroupId) &&
|
||||||
|
storage.getGroupAvatarDownloadJob(openGroup.server, openGroup.room, existingOpenGroup.imageId) == null
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
JobQueue.shared.add(GroupAvatarDownloadJob(roomToken, server))
|
JobQueue.shared.add(GroupAvatarDownloadJob(server, roomToken, existingOpenGroup.imageId))
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
pollInfo.details != null &&
|
||||||
|
pollInfo.details.imageId == null &&
|
||||||
|
existingOpenGroup.imageId != null
|
||||||
|
) {
|
||||||
|
storage.removeProfilePicture(dbGroupId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ data class SnodeMessage(
|
|||||||
*/
|
*/
|
||||||
val timestamp: Long
|
val timestamp: Long
|
||||||
) {
|
) {
|
||||||
|
internal constructor(): this("", "", -1, -1)
|
||||||
|
|
||||||
internal fun toJSON(): Map<String, String> {
|
internal fun toJSON(): Map<String, String> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
|
54
libsession/src/main/res/values-fr-rFR/strings.xml
Normal file
54
libsession/src/main/res/values-fr-rFR/strings.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- MessageRecord -->
|
||||||
|
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
|
||||||
|
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
|
||||||
|
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
|
||||||
|
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
|
||||||
|
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
|
||||||
|
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
|
||||||
|
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
|
||||||
|
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
|
||||||
|
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
|
||||||
|
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
|
||||||
|
<string name="MessageRecord_you">Vous</string>
|
||||||
|
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||||
|
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||||
|
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||||
|
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
|
||||||
|
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
|
||||||
|
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini l’expiration des messages éphémères à %1$s</string>
|
||||||
|
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini l’expiration des messages éphémères à %2$s</string>
|
||||||
|
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||||
|
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||||
|
<!-- expiration -->
|
||||||
|
<string name="expiration_off">Désactivé</string>
|
||||||
|
<plurals name="expiration_seconds">
|
||||||
|
<item quantity="one">%d seconde</item>
|
||||||
|
<item quantity="other">%d secondes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_seconds_abbreviated">%d s</string>
|
||||||
|
<plurals name="expiration_minutes">
|
||||||
|
<item quantity="one">%d minute</item>
|
||||||
|
<item quantity="other">%d minutes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_minutes_abbreviated">%d min</string>
|
||||||
|
<plurals name="expiration_hours">
|
||||||
|
<item quantity="one">%d heure</item>
|
||||||
|
<item quantity="other">%d heures</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_hours_abbreviated">%d h</string>
|
||||||
|
<plurals name="expiration_days">
|
||||||
|
<item quantity="one">%d jour</item>
|
||||||
|
<item quantity="other">%d jours</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_days_abbreviated">%d j</string>
|
||||||
|
<plurals name="expiration_weeks">
|
||||||
|
<item quantity="one">%d semaine</item>
|
||||||
|
<item quantity="other">%d semaines</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_weeks_abbreviated">%d sem</string>
|
||||||
|
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
|
||||||
|
<!-- RecipientProvider -->
|
||||||
|
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
|
||||||
|
</resources>
|
54
libsession/src/main/res/values-fr/strings.xml
Normal file
54
libsession/src/main/res/values-fr/strings.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- MessageRecord -->
|
||||||
|
<string name="MessageRecord_left_group">Vous avez quitté le groupe.</string>
|
||||||
|
<string name="MessageRecord_you_created_a_new_group">Vous avez créé un nouveau groupe.</string>
|
||||||
|
<string name="MessageRecord_s_added_you_to_the_group">%1$s vous a ajouté·e dans le groupe.</string>
|
||||||
|
<string name="MessageRecord_you_renamed_the_group_to_s">Vous avez renommé le groupe en %1$s</string>
|
||||||
|
<string name="MessageRecord_s_renamed_the_group_to_s">%1$s a renommé le groupe en : %2$s</string>
|
||||||
|
<string name="MessageRecord_you_added_s_to_the_group">Vous avez ajouté %1$s au groupe.</string>
|
||||||
|
<string name="MessageRecord_s_added_s_to_the_group">%1$s a ajouté %2$s au groupe.</string>
|
||||||
|
<string name="MessageRecord_you_removed_s_from_the_group">Vous avez retiré %1$s du groupe.</string>
|
||||||
|
<string name="MessageRecord_s_removed_s_from_the_group">%1$s a supprimé %2$s du groupe.</string>
|
||||||
|
<string name="MessageRecord_you_were_removed_from_the_group">Vous avez été retiré·e du groupe.</string>
|
||||||
|
<string name="MessageRecord_you">Vous</string>
|
||||||
|
<string name="MessageRecord_s_called_you">%s vous a appelé·e</string>
|
||||||
|
<string name="MessageRecord_called_s">Vous avez appelé %s</string>
|
||||||
|
<string name="MessageRecord_missed_call_from">Appel manqué de %s</string>
|
||||||
|
<string name="MessageRecord_you_disabled_disappearing_messages">Vous avez désactivé les messages éphémères.</string>
|
||||||
|
<string name="MessageRecord_s_disabled_disappearing_messages">%1$s a désactivé les messages éphémères.</string>
|
||||||
|
<string name="MessageRecord_you_set_disappearing_message_time_to_s">Vous avez défini l’expiration des messages éphémères à %1$s</string>
|
||||||
|
<string name="MessageRecord_s_set_disappearing_message_time_to_s">%1$s a défini l’expiration des messages éphémères à %2$s</string>
|
||||||
|
<string name="MessageRecord_s_took_a_screenshot">%1$s a pris une capture d\'écran.</string>
|
||||||
|
<string name="MessageRecord_media_saved_by_s">%1$s a enregistré le média.</string>
|
||||||
|
<!-- expiration -->
|
||||||
|
<string name="expiration_off">Désactivé</string>
|
||||||
|
<plurals name="expiration_seconds">
|
||||||
|
<item quantity="one">%d seconde</item>
|
||||||
|
<item quantity="other">%d secondes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_seconds_abbreviated">%d s</string>
|
||||||
|
<plurals name="expiration_minutes">
|
||||||
|
<item quantity="one">%d minute</item>
|
||||||
|
<item quantity="other">%d minutes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_minutes_abbreviated">%d min</string>
|
||||||
|
<plurals name="expiration_hours">
|
||||||
|
<item quantity="one">%d heure</item>
|
||||||
|
<item quantity="other">%d heures</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_hours_abbreviated">%d h</string>
|
||||||
|
<plurals name="expiration_days">
|
||||||
|
<item quantity="one">%d jour</item>
|
||||||
|
<item quantity="other">%d jours</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_days_abbreviated">%d j</string>
|
||||||
|
<plurals name="expiration_weeks">
|
||||||
|
<item quantity="one">%d semaine</item>
|
||||||
|
<item quantity="other">%d semaines</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="expiration_weeks_abbreviated">%d sem</string>
|
||||||
|
<string name="ConversationItem_group_action_left">%1$s a quitté le groupe.</string>
|
||||||
|
<!-- RecipientProvider -->
|
||||||
|
<string name="RecipientProvider_unnamed_group">Groupe sans nom</string>
|
||||||
|
</resources>
|
@ -4,4 +4,5 @@ interface LokiOpenGroupDatabaseProtocol {
|
|||||||
|
|
||||||
fun updateTitle(groupID: String, newValue: String)
|
fun updateTitle(groupID: String, newValue: String)
|
||||||
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
fun updateProfilePicture(groupID: String, newValue: ByteArray)
|
||||||
|
fun removeProfilePicture(groupID: String)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user