Merge remote-tracking branch 'origin/dev' into closed_groups

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
#	app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
#	app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
#	app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
#	app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt
#	app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
#	app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt
#	app/src/main/res/layout/view_control_message.xml
#	app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt
#	libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt
#	libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
#	libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt
#	libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt
This commit is contained in:
SessionHero01 2024-10-15 14:17:54 +11:00
commit 6814f0abe2
No known key found for this signature in database
136 changed files with 1453 additions and 774 deletions

View File

@ -54,8 +54,8 @@
"channel_id":"" "channel_id":""
}, },
"edukit":{ "edukit":{
"edu_url":"edukit.edu.cloud.huawei.com.cn", "edu_url":"edukit.cloud.huawei.com.cn",
"dh_url":"edukit.edu.cloud.huawei.com.cn" "dh_url":"edukit.cloud.huawei.com.cn"
}, },
"search":{ "search":{
"url":"https://search-dre.cloud.huawei.com" "url":"https://search-dre.cloud.huawei.com"

View File

@ -20,8 +20,8 @@ class HuaweiPushService: HmsMessageService() {
override fun onMessageReceived(message: RemoteMessage?) { override fun onMessageReceived(message: RemoteMessage?) {
Log.d(TAG, "onMessageReceived") Log.d(TAG, "onMessageReceived")
message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPush) ?: message?.dataOfMap?.takeIf { it.isNotEmpty() }?.let(pushReceiver::onPushDataReceived) ?:
pushReceiver.onPush(message?.data?.let(Base64::decode)) pushReceiver.onPushDataReceived(message?.data?.let(Base64::decode))
} }
override fun onNewToken(token: String?) { override fun onNewToken(token: String?) {

View File

@ -24,6 +24,8 @@ class HuaweiTokenFetcher @Inject constructor(
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run { override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
// https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370 // https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/push-basic-capability#h2-1576218800370
// getToken may return an empty string, if so HuaweiPushService#onNewToken will be called. // getToken may return an empty string, if so HuaweiPushService#onNewToken will be called.
withContext(Dispatchers.IO) { getToken(APP_ID, TOKEN_SCOPE) } withContext(Dispatchers.IO) {
getToken(APP_ID, TOKEN_SCOPE)
}
} }
} }

View File

@ -73,6 +73,7 @@ import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsession.utilities.recipients.RecipientModifiedListener; import org.session.libsession.utilities.recipients.RecipientModifiedListener;
import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.MediaView; import org.thoughtcrime.securesms.components.MediaView;
import org.thoughtcrime.securesms.components.dialogs.DeleteMediaPreviewDialog;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;

View File

@ -6,6 +6,7 @@ import com.google.protobuf.ByteString
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.messages.MarkAsDeletedMessage
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
@ -198,7 +199,6 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
} }
override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) { override fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase() val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase() else DatabaseComponent.get(context).mmsDatabase()
@ -215,34 +215,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) } threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
} }
override fun updateMessageAsDeleted(messageId: Long, isSms: Boolean): Long { override fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String) {
val messagingDatabase: MessagingDatabase =
if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
val isOutgoing = messagingDatabase.isOutgoing(messageId)
messagingDatabase.markAsDeleted(messageId)
if (isOutgoing) {
messagingDatabase.deleteMessage(messageId)
}
return messageId
}
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
val database = DatabaseComponent.get(context).mmsSmsDatabase() val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author) val address = Address.fromSerialized(author)
val message = database.getMessageFor(timestamp, address) ?: return null val message = database.getMessageFor(timestamp, address) ?: return Log.w("", "Failed to find message to mark as deleted")
updateMessageAsDeleted(message.id, !message.isMms)
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
messagingDatabase.markAsDeleted(message.id)
if (message.isOutgoing) {
messagingDatabase.deleteMessage(message.id)
}
return message.id markMessagesAsDeleted(
messages = listOf(MarkAsDeletedMessage(
messageId = message.id,
isOutgoing = message.isOutgoing
)),
isSms = !message.isMms,
displayedMessage = displayedMessage
)
}
override fun markMessagesAsDeleted(
messages: List<MarkAsDeletedMessage>,
isSms: Boolean,
displayedMessage: String
) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
messages.forEach { message ->
messagingDatabase.markAsDeleted(message.messageId, message.isOutgoing, displayedMessage)
}
} }
override fun getServerHashForMessage(messageID: Long, mms: Boolean): String? = override fun getServerHashForMessage(messageID: Long, mms: Boolean): String? =

View File

@ -1,7 +1,8 @@
package org.thoughtcrime.securesms package org.thoughtcrime.securesms.components.dialogs
import android.content.Context import android.content.Context
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.showSessionDialog
class DeleteMediaDialog { class DeleteMediaDialog {
companion object { companion object {

View File

@ -1,7 +1,8 @@
package org.thoughtcrime.securesms package org.thoughtcrime.securesms.components.dialogs
import android.content.Context import android.content.Context
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.showSessionDialog
class DeleteMediaPreviewDialog { class DeleteMediaPreviewDialog {
companion object { companion object {

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
/** /**
* Represents an action to be rendered * Represents an action to be rendered
*/ */

View File

@ -33,9 +33,8 @@ import android.widget.Toast
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
@ -120,6 +119,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity import org.thoughtcrime.securesms.conversation.disappearingmessages.DisappearingMessagesActivity
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.*
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
@ -141,6 +141,7 @@ import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper
import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
@ -184,8 +185,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
@ -257,8 +256,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java) .get(LinkPreviewViewModel::class.java)
} }
private var openLinkDialogUrl: String? by mutableStateOf(null)
private val threadId: Long by lazy { private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L) var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) { if (threadId == -1L) {
@ -361,9 +358,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (!viewModel.isMessageRequestThread && if (!viewModel.isMessageRequestThread &&
viewModel.canReactToMessages viewModel.canReactToMessages
) { ) {
showEmojiPicker(message, view) showConversationReaction(message, view)
} else { } else {
handleLongPress(message, position) selectMessage(message, position)
} }
}, },
onDeselect = { message, position -> onDeselect = { message, position ->
@ -424,7 +421,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// endregion // endregion
fun showOpenUrlDialog(url: String){ fun showOpenUrlDialog(url: String){
openLinkDialogUrl = url viewModel.onCommand(ShowOpenUrlDialog(url))
} }
// region Lifecycle // region Lifecycle
@ -437,16 +434,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.dialogOpenUrl.apply { binding.dialogOpenUrl.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
SessionMaterialTheme { val dialogsState by viewModel.dialogsState.collectAsState()
if(!openLinkDialogUrl.isNullOrEmpty()){ ConversationV2Dialogs(
OpenURLAlertDialog( dialogsState = dialogsState,
url = openLinkDialogUrl!!, sendCommand = viewModel::onCommand
onDismissRequest = { )
openLinkDialogUrl = null
}
)
}
}
} }
} }
@ -456,7 +448,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val recipient = viewModel.recipient val recipient = viewModel.recipient
val openGroup = recipient.let { viewModel.openGroup } val openGroup = recipient.let { viewModel.openGroup }
if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) { if (recipient == null || (recipient.isCommunityRecipient && openGroup == null)) {
Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() Toast.makeText(this, getString(R.string.conversationsDeleted), Toast.LENGTH_LONG).show()
return finish() return finish()
} }
@ -675,6 +667,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
recyclerScrollState = newState recyclerScrollState = newState
} }
}) })
lifecycleScope.launch {
viewModel.isAdmin.collect{
adapter.isAdmin = it
}
}
} }
private fun scrollToMostRecentMessageIfWeShould() { private fun scrollToMostRecentMessageIfWeShould() {
@ -912,6 +910,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
isVisible = state.showInput isVisible = state.showInput
showMediaControls = state.enableInputMediaControls showMediaControls = state.enableInputMediaControls
} }
// show or hide loading indicator
binding.loader.isVisible = uiState.showLoader
} }
} }
} }
@ -1356,7 +1357,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// `position` is the adapter position; not the visual position // `position` is the adapter position; not the visual position
private fun handleLongPress(message: MessageRecord, position: Int) { private fun selectMessage(message: MessageRecord, position: Int) {
val actionMode = this.actionMode val actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this) val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this)
actionModeCallback.delegate = this actionModeCallback.delegate = this
@ -1374,15 +1375,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
} }
private fun showEmojiPicker(message: MessageRecord, visibleMessageView: VisibleMessageView) { private fun showConversationReaction(message: MessageRecord, messageView: View) {
val messageContentView = when(messageView){
is VisibleMessageView -> messageView.messageContentView
is ControlMessageView -> messageView.controlContentView
else -> null
} ?: return Log.w(TAG, "Failed to show reaction because the messageRecord is not of a known type: $messageView")
val messageContentBitmap = try { val messageContentBitmap = try {
visibleMessageView.messageContentView.drawToBitmap() messageContentView.drawToBitmap()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Failed to show emoji picker", e) Log.e("Loki", "Failed to show emoji picker", e)
return return
} }
emojiPickerVisible = true emojiPickerVisible = true
ViewUtil.hideKeyboard(this, visibleMessageView) ViewUtil.hideKeyboard(this, messageView)
binding.reactionsShade.isVisible = true binding.reactionsShade.isVisible = true
binding.scrollToBottomButton.isVisible = false binding.scrollToBottomButton.isVisible = false
binding.conversationRecyclerView.suppressLayout(true) binding.conversationRecyclerView.suppressLayout(true)
@ -1404,14 +1411,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
}) })
val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) } val topLeft = intArrayOf(0, 0).also { messageContentView.getLocationInWindow(it) }
val selectedConversationModel = SelectedConversationModel( val selectedConversationModel = SelectedConversationModel(
messageContentBitmap, messageContentBitmap,
topLeft[0].toFloat(), topLeft[0].toFloat(),
topLeft[1].toFloat(), topLeft[1].toFloat(),
visibleMessageView.messageContentView.width, messageContentView.width,
message.isOutgoing, message.isOutgoing,
visibleMessageView.messageContentView messageContentView
) )
reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey) reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey)
} }
@ -2122,88 +2129,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun selectMessages(messages: Set<MessageRecord>) { override fun selectMessages(messages: Set<MessageRecord>) {
handleLongPress(messages.first(), 0) //TODO: begin selection mode selectMessage(messages.first(), 0) //TODO: begin selection mode
}
// The option to "Delete just for me" or "Delete for everyone"
private fun showDeleteOrDeleteForEveryoneInCommunityUI(messages: Set<MessageRecord>) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = viewModel.recipient!!
bottomSheet.onDeleteForMeTapped = {
messages.forEach(viewModel::deleteLocally)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onDeleteForEveryoneTapped = {
messages.forEach(viewModel::deleteForEveryone)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onCancelTapped = {
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
private fun showDeleteLocallyUI(messages: Set<MessageRecord>) {
showSessionDialog {
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getString(R.string.deleteMessagesDescriptionDevice))
button(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode)
}
} }
// Note: The messages in the provided set may be a single message, or multiple if there are a // Note: The messages in the provided set may be a single message, or multiple if there are a
// group of selected messages. // group of selected messages.
override fun deleteMessages(messages: Set<MessageRecord>) { override fun deleteMessages(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient viewModel.handleMessagesDeletion(messages)
if (recipient == null) { endActionMode()
Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
return
}
val allSentByCurrentUser = messages.all { it.isOutgoing }
val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null }
// If the recipient is a community OR a Note-to-Self then we delete the message for everyone
if (recipient.isCommunityRecipient || recipient.isLocalNumber) {
showSessionDialog {
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getString(R.string.deleteMessageDescriptionEveryone))
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteForEveryone); endActionMode() }
cancelButton { endActionMode() }
}
// Otherwise if this is a 1-on-1 conversation we may decided to delete just for ourselves or delete for everyone
} else if ((allSentByCurrentUser || viewModel.isClosedGroupAdmin) && allHasHash) {
val bottomSheet = DeleteOptionsBottomSheet()
bottomSheet.recipient = recipient
bottomSheet.onDeleteForMeTapped = {
messages.forEach(viewModel::deleteLocally)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onDeleteForEveryoneTapped = {
messages.forEach(viewModel::deleteForEveryone)
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.onCancelTapped = {
bottomSheet.dismiss()
endActionMode()
}
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
}
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
{
showSessionDialog {
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
text(resources.getString(R.string.deleteMessageDescriptionDevice))
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
cancelButton(::endActionMode)
}
}
} }
override fun banUser(messages: Set<MessageRecord>) { override fun banUser(messages: Set<MessageRecord>) {

View File

@ -5,6 +5,7 @@ import android.database.Cursor
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault import androidx.core.util.getOrDefault
@ -35,7 +36,7 @@ class ConversationAdapter(
private val isReversed: Boolean, private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit, private val onItemLongPress: (MessageRecord, Int, View) -> Unit,
private val onDeselect: (MessageRecord, Int) -> Unit, private val onDeselect: (MessageRecord, Int) -> Unit,
private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
private val glide: RequestManager, private val glide: RequestManager,
@ -44,6 +45,7 @@ class ConversationAdapter(
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() } private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() } private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
var selectedItems = mutableSetOf<MessageRecord>() var selectedItems = mutableSetOf<MessageRecord>()
var isAdmin: Boolean = false
private var searchQuery: String? = null private var searchQuery: String? = null
var visibleMessageViewDelegate: VisibleMessageViewDelegate? = null var visibleMessageViewDelegate: VisibleMessageViewDelegate? = null
@ -155,12 +157,18 @@ class ConversationAdapter(
} else { } else {
visibleMessageView.onPress = null visibleMessageView.onPress = null
visibleMessageView.onSwipeToReply = null visibleMessageView.onSwipeToReply = null
visibleMessageView.onLongPress = null // you can long press on "marked as deleted" messages
visibleMessageView.onLongPress =
{ onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) }
} }
} }
is ControlMessageViewHolder -> { is ControlMessageViewHolder -> {
viewHolder.view.bind(message, messageBefore) viewHolder.view.bind(
message = message,
previous = messageBefore,
longPress = { onItemLongPress(message, viewHolder.adapterPosition, viewHolder.view) }
)
} }
} }
} }

View File

@ -21,6 +21,7 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -222,7 +223,6 @@ class ConversationReactionOverlay : FrameLayout {
endScale = spaceAvailableForItem / conversationItemSnapshot.height endScale = spaceAvailableForItem / conversationItemSnapshot.height
endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1 endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale) endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
val contextMenuTop = endY + conversationItemSnapshot.height * endScale
reactionBarBackgroundY = reactionBarTopPadding //getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); reactionBarBackgroundY = reactionBarTopPadding //getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY);
endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale) endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
} else { } else {
@ -271,11 +271,17 @@ class ConversationReactionOverlay : FrameLayout {
revealAnimatorSet.start() revealAnimatorSet.start()
if (isWideLayout) { if (isWideLayout) {
val scrubberRight = scrubberX + scrubberWidth val scrubberRight = scrubberX + scrubberWidth
val offsetX = if (isMessageOnLeft) scrubberRight + menuPadding else scrubberX - contextMenu.getMaxWidth() - menuPadding val offsetX = when {
isMessageOnLeft -> scrubberRight + menuPadding
else -> scrubberX - contextMenu.getMaxWidth() - menuPadding
}
contextMenu.show(offsetX.toInt(), Math.min(backgroundView.y, (overlayHeight - contextMenu.getMaxHeight()).toFloat()).toInt()) contextMenu.show(offsetX.toInt(), Math.min(backgroundView.y, (overlayHeight - contextMenu.getMaxHeight()).toFloat()).toInt())
} else { } else {
val contentX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX val contentX = if (isMessageOnLeft) scrubberHorizontalMargin.toFloat() else selectedConversationModel.bubbleX
val offsetX = if (isMessageOnLeft) contentX else -contextMenu.getMaxWidth() + contentX + bubbleWidth val offsetX = when {
isMessageOnLeft -> contentX
else -> -contextMenu.getMaxWidth() + contentX + bubbleWidth
}
val menuTop = endApparentTop + conversationItemSnapshot.height * endScale val menuTop = endApparentTop + conversationItemSnapshot.height * endScale
contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt()) contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt())
} }
@ -526,19 +532,30 @@ class ConversationReactionOverlay : FrameLayout {
val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId) val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId)
?: return emptyList() ?: return emptyList()
val userPublicKey = getLocalNumber(context)!! val userPublicKey = getLocalNumber(context)!!
// control messages and "marked as deleted" messages can only delete
val isDeleteOnly = message.isDeleted || message.isControlMessage
// Select message // Select message
items += ActionItem(R.attr.menu_select_icon, R.string.select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select) if(!isDeleteOnly) {
items += ActionItem(
R.attr.menu_select_icon,
R.string.select,
{ handleActionItemClicked(Action.SELECT) },
R.string.AccessibilityId_select
)
}
// Reply // Reply
val canWrite = openGroup == null || openGroup.canWrite val canWrite = openGroup == null || openGroup.canWrite
if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) { if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation && !isDeleteOnly) {
items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply) items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
} }
// Copy message text // Copy message text
if (!containsControlMessage && hasText) { if (!containsControlMessage && hasText && !isDeleteOnly) {
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) }) items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
} }
// Copy Account ID // Copy Account ID
if (!recipient.isCommunityRecipient && message.isIncoming) { if (!recipient.isCommunityRecipient && message.isIncoming && !isDeleteOnly) {
items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) }) items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
} }
// Delete message // Delete message
@ -547,15 +564,20 @@ class ConversationReactionOverlay : FrameLayout {
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger)) R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
} }
// Ban user // Ban user
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) }) items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
} }
// Ban and delete all // Ban and delete all
if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !isDeleteOnly) {
items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) }) items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
} }
// Message detail // Message detail
items += ActionItem(R.attr.menu_info_icon, R.string.messageInfo, { handleActionItemClicked(Action.VIEW_INFO) }) if(!isDeleteOnly) {
items += ActionItem(
R.attr.menu_info_icon,
R.string.messageInfo,
{ handleActionItemClicked(Action.VIEW_INFO) })
}
// Resend // Resend
if (message.isFailed) { if (message.isFailed) {
items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) }) items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) })
@ -565,7 +587,7 @@ class ConversationReactionOverlay : FrameLayout {
items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) }) items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
} }
// Save media.. // Save media..
if (message.isMms) { if (message.isMms && !isDeleteOnly) {
// ..but only provide the save option if the there is a media attachment which has finished downloading. // ..but only provide the save option if the there is a media attachment which has finished downloading.
val mmsMessage = message as MediaMmsMessageRecord val mmsMessage = message as MediaMmsMessageRecord
if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) { if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) {
@ -576,8 +598,10 @@ class ConversationReactionOverlay : FrameLayout {
) )
} }
} }
backgroundView.visibility = VISIBLE
foregroundView.visibility = VISIBLE // deleted messages have no emoji reactions
backgroundView.isVisible = !isDeleteOnly
foregroundView.isVisible = !isDeleteOnly
return items return items
} }

View File

@ -0,0 +1,219 @@
package org.thoughtcrime.securesms.conversation.v2
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import network.loki.messenger.R
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteAllDevicesDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.HideDeleteEveryoneDialog
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedForEveryone
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.MarkAsDeletedLocally
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel.Commands.ShowOpenUrlDialog
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.OpenURLAlertDialog
import org.thoughtcrime.securesms.ui.RadioOption
import org.thoughtcrime.securesms.ui.components.TitledRadioButton
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionMaterialTheme
@Composable
fun ConversationV2Dialogs(
dialogsState: ConversationViewModel.DialogsState,
sendCommand: (ConversationViewModel.Commands) -> Unit
){
SessionMaterialTheme {
// open link confirmation
if(!dialogsState.openLinkDialogUrl.isNullOrEmpty()){
OpenURLAlertDialog(
url = dialogsState.openLinkDialogUrl,
onDismissRequest = {
// hide dialog
sendCommand(ShowOpenUrlDialog(null))
}
)
}
// delete message(s) for everyone
if(dialogsState.deleteEveryone != null){
var deleteForEveryone by remember { mutableStateOf(dialogsState.deleteEveryone.defaultToEveryone)}
AlertDialog(
onDismissRequest = {
// hide dialog
sendCommand(HideDeleteEveryoneDialog)
},
title = pluralStringResource(
R.plurals.deleteMessage,
dialogsState.deleteEveryone.messages.size,
dialogsState.deleteEveryone.messages.size
),
text = pluralStringResource(
R.plurals.deleteMessageConfirm,
dialogsState.deleteEveryone.messages.size,
dialogsState.deleteEveryone.messages.size
),
content = {
// add warning text, if any
dialogsState.deleteEveryone.warning?.let {
Text(
text = it,
textAlign = TextAlign.Center,
style = LocalType.current.small,
color = LocalColors.current.warning,
modifier = Modifier.padding(
top = LocalDimensions.current.xxxsSpacing,
bottom = LocalDimensions.current.xxsSpacing
)
)
}
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDeviceOnly)),
selected = !deleteForEveryone
)
) {
deleteForEveryone = false
}
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageEveryone)),
selected = deleteForEveryone,
enabled = dialogsState.deleteEveryone.everyoneEnabled
)
) {
deleteForEveryone = true
}
},
buttons = listOf(
DialogButtonModel(
text = GetString(stringResource(id = R.string.delete)),
color = LocalColors.current.danger,
onClick = {
// delete messages based on chosen option
sendCommand(
if(deleteForEveryone) MarkAsDeletedForEveryone(
dialogsState.deleteEveryone.copy(defaultToEveryone = deleteForEveryone)
)
else MarkAsDeletedLocally(dialogsState.deleteEveryone.messages)
)
}
),
DialogButtonModel(
GetString(stringResource(R.string.cancel))
)
)
)
}
// delete message(s) for all my devices
if(dialogsState.deleteAllDevices != null){
var deleteAllDevices by remember { mutableStateOf(dialogsState.deleteAllDevices.defaultToEveryone) }
AlertDialog(
onDismissRequest = {
// hide dialog
sendCommand(HideDeleteAllDevicesDialog)
},
title = pluralStringResource(
R.plurals.deleteMessage,
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
text = pluralStringResource(
R.plurals.deleteMessageConfirm,
dialogsState.deleteAllDevices.messages.size,
dialogsState.deleteAllDevices.messages.size
),
content = {
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDeviceOnly)),
selected = !deleteAllDevices
)
) {
deleteAllDevices = false
}
TitledRadioButton(
contentPadding = PaddingValues(
horizontal = LocalDimensions.current.xxsSpacing,
vertical = 0.dp
),
option = RadioOption(
value = Unit,
title = GetString(stringResource(R.string.deleteMessageDevicesAll)),
selected = deleteAllDevices
)
) {
deleteAllDevices = true
}
},
buttons = listOf(
DialogButtonModel(
text = GetString(stringResource(id = R.string.delete)),
color = LocalColors.current.danger,
onClick = {
// delete messages based on chosen option
sendCommand(
if(deleteAllDevices) MarkAsDeletedForEveryone(
dialogsState.deleteAllDevices.copy(defaultToEveryone = deleteAllDevices)
)
else MarkAsDeletedLocally(dialogsState.deleteAllDevices.messages)
)
}
),
DialogButtonModel(
GetString(stringResource(R.string.cancel))
)
)
)
}
}
}
@Preview
@Composable
fun PreviewURLDialog(){
PreviewTheme {
ConversationV2Dialogs(
dialogsState = ConversationViewModel.DialogsState(
openLinkDialogUrl = "https://google.com"
),
sendCommand = {}
)
}
}

View File

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import android.widget.Toast
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -12,13 +14,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.libsession_util.util.GroupMember import network.loki.messenger.libsession_util.util.GroupMember
import org.session.libsession.database.MessageDataProvider import org.session.libsession.database.MessageDataProvider
@ -29,27 +31,37 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.getType
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.thoughtcrime.securesms.audio.AudioSlidePlayer import org.thoughtcrime.securesms.audio.AudioSlidePlayer
import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.LokiMessageDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.model.MessageRecord 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.groups.OpenGroupManager import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID import java.util.UUID
class ConversationViewModel( class ConversationViewModel(
val threadId: Long, val threadId: Long,
val edKeyPair: KeyPair?, val edKeyPair: KeyPair?,
private val application: Application,
private val repository: ConversationRepository, private val repository: ConversationRepository,
private val storage: StorageProtocol, private val storage: StorageProtocol,
private val messageDataProvider: MessageDataProvider, private val messageDataProvider: MessageDataProvider,
private val groupDb: GroupDatabase, private val groupDb: GroupDatabase,
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
) : ViewModel() { ) : ViewModel() {
val showSendAfterApprovalText: Boolean val showSendAfterApprovalText: Boolean
@ -58,8 +70,44 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState()) private val _uiState = MutableStateFlow(ConversationUiState())
val uiState: StateFlow<ConversationUiState> get() = _uiState val uiState: StateFlow<ConversationUiState> get() = _uiState
private val _dialogsState = MutableStateFlow(DialogsState())
val dialogsState: StateFlow<DialogsState> = _dialogsState
private val _isAdmin = MutableStateFlow(false)
val isAdmin: StateFlow<Boolean> = _isAdmin
private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce { private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce {
repository.maybeGetRecipientForThreadId(threadId) val conversation = repository.maybeGetRecipientForThreadId(threadId)
// set admin from current conversation
val conversationType = conversation?.getType()
// Determining is the current user is an admin will depend on the kind of conversation we are in
_isAdmin.value = when(conversationType) {
// for Groups V2
MessageType.GROUPS_V2 -> {
//todo GROUPS V2 add logic where code is commented to determine if user is an admin
false // FANCHAO - properly set up admin for groups v2 here
}
// for legacy groups, check if the user created the group
MessageType.LEGACY_GROUP -> {
// for legacy groups, we check if the current user is the one who created the group
run {
val localUserAddress =
textSecurePreferences.getLocalNumber() ?: return@run false
val group = storage.getGroup(conversation.address.toGroupString())
group?.admins?.contains(fromSerialized(localUserAddress)) ?: false
}
}
// for communities the the `isUserModerator` field
MessageType.COMMUNITY -> isUserCommunityManager()
// false in other cases
else -> false
}
conversation
} }
val expirationConfiguration: ExpirationConfiguration? val expirationConfiguration: ExpirationConfiguration?
get() = storage.getExpirationConfiguration(threadId) get() = storage.getExpirationConfiguration(threadId)
@ -274,50 +322,409 @@ class ConversationViewModel(
repository.deleteThread(threadId) repository.deleteThread(threadId)
} }
fun deleteLocally(message: MessageRecord) { fun handleMessagesDeletion(messages: Set<MessageRecord>){
stopPlayingAudioMessage(message) val conversation = recipient
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action") if (conversation == null) {
repository.deleteLocally(recipient, message) Log.w("ConversationActivityV2", "Asked to delete messages but could not obtain viewModel recipient - aborting.")
return
}
viewModelScope.launch(Dispatchers.IO) {
val allSentByCurrentUser = messages.all { it.isOutgoing }
val conversationType = conversation.getType()
// hashes are required if wanting to delete messages from the 'storage server'
// They are not required for communities OR if all messages are outgoing
// also we can only delete deleted messages (marked as deleted) locally
val canDeleteForEveryone = messages.all{ !it.isDeleted && !it.isControlMessage } && (
messages.all { it.isOutgoing } ||
conversationType == MessageType.COMMUNITY ||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null
})
// There are three types of dialogs for deletion:
// 1- Delete on device only OR all devices - Used for Note to self
// 2- Delete on device only OR for everyone - Used for 'admins' or a user's own messages, as long as the message have a server hash
// 3- Delete on device only - Used otherwise
when {
// the conversation is a note to self
conversationType == MessageType.NOTE_TO_SELF -> {
_dialogsState.update {
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = false,
everyoneEnabled = true,
messageType = conversationType
)
)
}
}
// If the user is an admin or is interacting with their own message And are allowed to delete for everyone
(isAdmin.value || allSentByCurrentUser) && canDeleteForEveryone -> {
_dialogsState.update {
it.copy(
deleteEveryone = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = isAdmin.value,
everyoneEnabled = true,
messageType = conversationType
)
)
}
}
// for non admins, users interacting with someone else's message, or control messages
else -> {
_dialogsState.update {
it.copy(
deleteEveryone = DeleteForEveryoneDialogData(
messages = messages,
defaultToEveryone = false,
everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case
messageType = conversationType,
warning = application.resources.getQuantityString(
R.plurals.deleteMessageWarning, messages.count(), messages.count()
)
)
)
}
}
}
}
} }
/**
* This delete the message locally only.
* Attachments and other related data will be removed from the db.
* If the messages were already marked as deleted they will be removed fully from the db,
* otherwise they will appear as a special type of message
* that says something like "This message was deleted"
*/
fun deleteLocally(messages: Set<MessageRecord>) {
// make sure to stop audio messages, if any
messages.filterIsInstance<MmsMessageRecord>()
.mapNotNull { it.slideDeck.audioSlide }
.forEach(::stopMessageAudio)
// if the message was already marked as deleted or control messages, remove it from the db instead
if(messages.all { it.isDeleted || it.isControlMessage }){
// Remove the message locally (leave nothing behind)
repository.deleteMessages(messages = messages, threadId = threadId)
} else {
// only mark as deleted (message remains behind with "This message was deleted on this device" )
repository.markAsDeletedLocally(
messages = messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedLocally)
)
}
// show confirmation toast
Toast.makeText(
application,
application.resources.getQuantityString(R.plurals.deleteMessageDeleted, messages.count(), messages.count()),
Toast.LENGTH_SHORT
).show()
}
/**
* This will mark the messages as deleted, for everyone.
* Attachments and other related data will be removed from the db,
* but the messages themselves won't be removed from the db.
* Instead they will appear as a special type of message
* that says something like "This message was deleted"
*/
private fun markAsDeletedForEveryone(
data: DeleteForEveryoneDialogData
) = viewModelScope.launch {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
// make sure to stop audio messages, if any
data.messages.filterIsInstance<MmsMessageRecord>()
.mapNotNull { it.slideDeck.audioSlide }
.forEach(::stopMessageAudio)
// the exact logic for this will depend on the messages type
when(data.messageType){
MessageType.NOTE_TO_SELF -> markAsDeletedForEveryoneNoteToSelf(data)
MessageType.ONE_ON_ONE -> markAsDeletedForEveryone1On1(data)
MessageType.LEGACY_GROUP -> markAsDeletedForEveryoneLegacyGroup(data.messages)
MessageType.GROUPS_V2 -> markAsDeletedForEveryoneGroupsV2(data)
MessageType.COMMUNITY -> markAsDeletedForEveryoneCommunity(data)
}
}
private fun markAsDeletedForEveryoneNoteToSelf(data: DeleteForEveryoneDialogData){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
repository.deleteNoteToSelfMessagesRemotely(threadId, recipient!!, data.messages)
// When this is done we simply need to remove the message locally (leave nothing behind)
repository.deleteMessages(messages = data.messages, threadId = threadId)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryone1On1(data: DeleteForEveryoneDialogData){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
repository.delete1on1MessagesRemotely(threadId, recipient!!, data.messages)
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryoneLegacyGroup(messages: Set<MessageRecord>){
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
viewModelScope.launch(Dispatchers.IO) {
// delete remotely
try {
repository.deleteLegacyGroupMessagesRemotely(recipient!!, messages)
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
messages.count(),
messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
messages.size,
messages.size
), Toast.LENGTH_SHORT
).show()
}
}
}
}
private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
//todo GROUPS V2 - uncomment below and use Fanchao's method to delete a group V2
try {
//repository.callMethodFromFanchao(threadId, recipient, data.messages)
// the repo will handle the internal logic (calling `/delete` on the swarm
// and sending 'GroupUpdateDeleteMemberContentMessage'
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(), data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteAllDevices = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun markAsDeletedForEveryoneCommunity(data: DeleteForEveryoneDialogData){
viewModelScope.launch(Dispatchers.IO) {
// show a loading indicator
_uiState.update { it.copy(showLoader = true) }
// delete remotely
try {
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
// When this is done we simply need to remove the message locally
repository.markAsDeletedLocally(
messages = data.messages,
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
)
// show confirmation toast
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageDeleted,
data.messages.count(),
data.messages.count()
),
Toast.LENGTH_SHORT
).show()
}
} catch (e: Exception) {
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
// failed to delete - show a toast and get back on the modal
withContext(Dispatchers.Main) {
Toast.makeText(
application,
application.resources.getQuantityString(
R.plurals.deleteMessageFailed,
data.messages.size,
data.messages.size
), Toast.LENGTH_SHORT
).show()
}
_dialogsState.update { it.copy(deleteEveryone = data) }
}
// hide loading indicator
_uiState.update { it.copy(showLoader = false) }
}
}
private fun isUserCommunityManager() = openGroup?.let { openGroup ->
val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false
OpenGroupManager.isUserModerator(application, openGroup.id, userPublicKey, blindedPublicKey)
} ?: false
/** /**
* Stops audio player if its current playing is the one given in the message. * Stops audio player if its current playing is the one given in the message.
*/ */
private fun stopPlayingAudioMessage(message: MessageRecord) { private fun stopMessageAudio(message: MessageRecord) {
val mmsMessage = message as? MmsMessageRecord ?: return val mmsMessage = message as? MmsMessageRecord ?: return
val audioSlide = mmsMessage.slideDeck.audioSlide ?: return val audioSlide = mmsMessage.slideDeck.audioSlide ?: return
stopMessageAudio(audioSlide)
}
private fun stopMessageAudio(audioSlide: AudioSlide) {
AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop() AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop()
} }
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch { fun setRecipientApproved() {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.") val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
stopPlayingAudioMessage(message) repository.setApproved(recipient, true)
repository.deleteForEveryone(threadId, recipient, message)
.onSuccess {
Log.d("Loki", "Deleted message ${message.id} ")
stopPlayingAudioMessage(message)
}
.onFailure {
Log.w("Loki", "FAILED TO delete message ${message.id} ")
showMessage("Couldn't delete message due to error: $it")
}
}
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) = viewModelScope.launch {
repository.deleteMessageWithoutUnsendRequest(threadId, messages)
.onFailure {
showMessage("Couldn't delete message due to error: $it")
}
} }
fun banUser(recipient: Recipient) = viewModelScope.launch { fun banUser(recipient: Recipient) = viewModelScope.launch {
repository.banUser(threadId, recipient) repository.banUser(threadId, recipient)
.onSuccess { .onSuccess {
showMessage("Successfully banned user") showMessage(application.getString(R.string.banUserBanned))
} }
.onFailure { .onFailure {
showMessage("Couldn't ban user due to error: $it") showMessage(application.getString(R.string.banErrorFailed))
} }
} }
@ -326,13 +733,13 @@ class ConversationViewModel(
repository.banAndDeleteAll(threadId, messageRecord.individualRecipient) repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
.onSuccess { .onSuccess {
// At this point the server side messages have been successfully deleted.. // At this point the server side messages have been successfully deleted..
showMessage("Successfully banned user and deleted all their messages") showMessage(application.getString(R.string.banUserBanned))
// ..so we can now delete all their messages in this thread from local storage & remove the views. // ..so we can now delete all their messages in this thread from local storage & remove the views.
repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord) repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
} }
.onFailure { .onFailure {
showMessage("Couldn't execute request due to error: $it") showMessage(application.getString(R.string.banErrorFailed))
} }
} }
@ -431,6 +838,40 @@ class ConversationViewModel(
} }
} }
fun onCommand(command: Commands) {
when (command) {
is Commands.ShowOpenUrlDialog -> {
_dialogsState.update {
it.copy(openLinkDialogUrl = command.url)
}
}
is Commands.HideDeleteEveryoneDialog -> {
_dialogsState.update {
it.copy(deleteEveryone = null)
}
}
is Commands.HideDeleteAllDevicesDialog -> {
_dialogsState.update {
it.copy(deleteAllDevices = null)
}
}
is Commands.MarkAsDeletedLocally -> {
// hide dialog first
_dialogsState.update {
it.copy(deleteEveryone = null)
}
deleteLocally(command.messages)
}
is Commands.MarkAsDeletedForEveryone -> {
markAsDeletedForEveryone(command.data)
}
}
}
@dagger.assisted.AssistedFactory @dagger.assisted.AssistedFactory
interface AssistedFactory { interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?): Factory fun create(threadId: Long, edKeyPair: KeyPair?): Factory
@ -440,6 +881,7 @@ class ConversationViewModel(
class Factory @AssistedInject constructor( class Factory @AssistedInject constructor(
@Assisted private val threadId: Long, @Assisted private val threadId: Long,
@Assisted private val edKeyPair: KeyPair?, @Assisted private val edKeyPair: KeyPair?,
private val application: Application,
private val repository: ConversationRepository, private val repository: ConversationRepository,
private val storage: StorageProtocol, private val storage: StorageProtocol,
private val messageDataProvider: MessageDataProvider, private val messageDataProvider: MessageDataProvider,
@ -447,20 +889,48 @@ class ConversationViewModel(
private val threadDb: ThreadDatabase, private val threadDb: ThreadDatabase,
@ApplicationContext @ApplicationContext
private val context: Context, private val context: Context,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationViewModel( return ConversationViewModel(
threadId = threadId, threadId = threadId,
edKeyPair = edKeyPair, edKeyPair = edKeyPair,
application = application,
repository = repository, repository = repository,
storage = storage, storage = storage,
messageDataProvider = messageDataProvider, messageDataProvider = messageDataProvider,
groupDb = groupDb, groupDb = groupDb,
threadDb = threadDb, threadDb = threadDb,
lokiMessageDb = lokiMessageDb,
textSecurePreferences = textSecurePreferences
) as T ) as T
} }
} }
data class DialogsState(
val openLinkDialogUrl: String? = null,
val deleteEveryone: DeleteForEveryoneDialogData? = null,
val deleteAllDevices: DeleteForEveryoneDialogData? = null,
)
data class DeleteForEveryoneDialogData(
val messages: Set<MessageRecord>,
val messageType: MessageType,
val defaultToEveryone: Boolean,
val everyoneEnabled: Boolean,
val warning: String? = null
)
sealed class Commands {
data class ShowOpenUrlDialog(val url: String?) : Commands()
data object HideDeleteEveryoneDialog : Commands()
data object HideDeleteAllDevicesDialog : Commands()
data class MarkAsDeletedLocally(val messages: Set<MessageRecord>): Commands()
data class MarkAsDeletedForEveryone(val data: DeleteForEveryoneDialogData): Commands()
}
} }
data class UiMessage(val id: Long, val message: String) data class UiMessage(val id: Long, val message: String)
@ -471,6 +941,7 @@ data class ConversationUiState(
val shouldExit: Boolean = false, val shouldExit: Boolean = false,
val showInput: Boolean = true, val showInput: Boolean = true,
val enableInputMediaControls: Boolean = true, val enableInputMediaControls: Boolean = true,
val showLoader: Boolean = false
) )
sealed interface MessageRequestUiState { sealed interface MessageRequestUiState {

View File

@ -77,7 +77,9 @@ class InputBarButton : RelativeLayout {
result.layoutParams = LayoutParams(size, size) result.layoutParams = LayoutParams(size, size)
result.scaleType = ImageView.ScaleType.CENTER_INSIDE result.scaleType = ImageView.ScaleType.CENTER_INSIDE
result.setImageResource(iconID) result.setImageResource(iconID)
result.imageTintList = ColorStateList.valueOf(context.getColorFromAttr(R.attr.input_bar_button_text_color)) result.imageTintList = if(isSendButton)
ColorStateList.valueOf(context.getColorFromAttr(R.attr.message_sent_text_color))
else ColorStateList.valueOf(context.getColorFromAttr(R.attr.input_bar_button_text_color))
result result
} }

View File

@ -10,6 +10,7 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.AccountId
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
@ -43,6 +44,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
// Embedded function // Embedded function
fun userCanDeleteSelectedItems(): Boolean { fun userCanDeleteSelectedItems(): Boolean {
// admin can delete all combinations
if(adapter.isAdmin) return true
val allSentByCurrentUser = selectedItems.all { it.isOutgoing } val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing } val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser } if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
@ -92,7 +96,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
} }
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
val selectedItems = adapter.selectedItems val selectedItems = adapter.selectedItems.toSet()
when (item.itemId) { when (item.itemId) {
R.id.menu_context_delete_message -> delegate?.deleteMessages(selectedItems) R.id.menu_context_delete_message -> delegate?.deleteMessages(selectedItems)
R.id.menu_context_ban_user -> delegate?.banUser(selectedItems) R.id.menu_context_ban_user -> delegate?.banUser(selectedItems)

View File

@ -6,6 +6,7 @@ import android.content.Intent
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone import androidx.core.view.isGone
@ -55,11 +56,13 @@ class ControlMessageView : LinearLayout {
@Inject lateinit var disappearingMessages: DisappearingMessages @Inject lateinit var disappearingMessages: DisappearingMessages
val controlContentView: View get() = binding.controlContentView
init { init {
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
} }
fun bind(message: MessageRecord, previous: MessageRecord?) { fun bind(message: MessageRecord, previous: MessageRecord?, longPress: (() -> Unit)? = null) {
binding.dateBreakTextView.showDateBreak(message, previous) binding.dateBreakTextView.showDateBreak(message, previous)
binding.iconImageView.isGone = true binding.iconImageView.isGone = true
binding.expirationTimerView.isGone = true binding.expirationTimerView.isGone = true
@ -86,7 +89,7 @@ class ControlMessageView : LinearLayout {
&& message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE) && message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
&& threadRecipient?.isGroupRecipient != true && threadRecipient?.isGroupRecipient != true
followSetting.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) } binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
} }
} }
message.isMediaSavedNotification -> { message.isMediaSavedNotification -> {
@ -130,7 +133,7 @@ class ControlMessageView : LinearLayout {
} }
// remove clicks by default // remove clicks by default
setOnClickListener(null) binding.controlContentView.setOnClickListener(null)
hideInfo() hideInfo()
// handle click behaviour depending on criteria // handle click behaviour depending on criteria
@ -140,7 +143,7 @@ class ControlMessageView : LinearLayout {
// show a dedicated privacy dialog // show a dedicated privacy dialog
!TextSecurePreferences.isCallNotificationsEnabled(context) -> { !TextSecurePreferences.isCallNotificationsEnabled(context) -> {
showInfo() showInfo()
setOnClickListener { binding.controlContentView.setOnClickListener {
context.showSessionDialog { context.showSessionDialog {
val titleTxt = context.getSubbedString( val titleTxt = context.getSubbedString(
R.string.callsMissedCallFrom, R.string.callsMissedCallFrom,
@ -167,7 +170,7 @@ class ControlMessageView : LinearLayout {
// show a dedicated permission dialog // show a dedicated permission dialog
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> { !Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> {
showInfo() showInfo()
setOnClickListener { binding.controlContentView.setOnClickListener {
context.showSessionDialog { context.showSessionDialog {
val titleTxt = context.getSubbedString( val titleTxt = context.getSubbedString(
R.string.callsMissedCallFrom, R.string.callsMissedCallFrom,
@ -207,6 +210,14 @@ class ControlMessageView : LinearLayout {
binding.textView.isGone = message.isCallLog binding.textView.isGone = message.isCallLog
binding.callView.isVisible = message.isCallLog binding.callView.isVisible = message.isCallLog
// handle long clicked if it was passed on
longPress?.let {
binding.controlContentView.setOnLongClickListener {
longPress.invoke()
true
}
}
} }
fun showInfo(){ fun showInfo(){

View File

@ -21,7 +21,8 @@ class DeletedMessageView : LinearLayout {
// region Updating // region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) { fun bind(message: MessageRecord, @ColorInt textColor: Int) {
assert(message.isDeleted) assert(message.isDeleted)
binding.deleteTitleTextView.text = context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1) // set the text to the message's body if it is set, else use a fallback
binding.deleteTitleTextView.text = message.body.ifEmpty { context.resources.getQuantityString(R.plurals.deleteMessageDeleted, 1, 1) }
binding.deleteTitleTextView.setTextColor(textColor) binding.deleteTitleTextView.setTextColor(textColor)
binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor) binding.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
} }

View File

@ -80,6 +80,7 @@ class VisibleMessageContentView : ConstraintLayout {
onContentDoubleTap = null onContentDoubleTap = null
if (message.isDeleted) { if (message.isDeleted) {
binding.contentParent.isVisible = true
binding.deletedMessageView.root.isVisible = true binding.deletedMessageView.root.isVisible = true
binding.deletedMessageView.root.bind(message, getTextColor(context, message)) binding.deletedMessageView.root.bind(message, getTextColor(context, message))
binding.bodyTextView.isVisible = false binding.bodyTextView.isVisible = false

View File

@ -279,13 +279,12 @@ class VisibleMessageView : FrameLayout {
// Get details regarding how we should display the message (it's delivery icon, icon tint colour, and // Get details regarding how we should display the message (it's delivery icon, icon tint colour, and
// the resource string for what text to display (R.string.delivery_status_sent etc.). // the resource string for what text to display (R.string.delivery_status_sent etc.).
val (iconID, iconColor, textId) = getMessageStatusInfo(message)
// If we get any nulls then a message isn't one with a state that we care about (i.e., control messages // If we get a null messageStatus then the message isn't one with a state that we care about (i.e., control messages
// etc.) - so bail. See: `DisplayRecord.is<WHATEVER>` for the full suite of message state methods. // etc.) - so bail. See: `DisplayRecord.is<WHATEVER>` for the full suite of message state methods.
// Also: We set all delivery status elements visibility to false just to make sure we don't display any // Also: We set all delivery status elements visibility to false just to make sure we don't display any
// stale data. // stale data.
if (textId == null) return val messageStatus = getMessageStatusInfo(message) ?: return
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> { binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
gravity = if (message.isOutgoing) Gravity.END else Gravity.START gravity = if (message.isOutgoing) Gravity.END else Gravity.START
@ -294,16 +293,17 @@ class VisibleMessageView : FrameLayout {
horizontalBias = if (message.isOutgoing) 1f else 0f horizontalBias = if (message.isOutgoing) 1f else 0f
} }
// If the message is incoming AND it is not scheduled to disappear then don't show any status or timer details // If the message is incoming AND it is not scheduled to disappear
// OR it is a deleted message then don't show any status or timer details
val scheduledToDisappear = message.expiresIn > 0 val scheduledToDisappear = message.expiresIn > 0
if (message.isIncoming && !scheduledToDisappear) return if (message.isDeleted || message.isIncoming && !scheduledToDisappear) return
// Set text & icons as appropriate for the message state. Note: Possible message states we care // Set text & icons as appropriate for the message state. Note: Possible message states we care
// about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent. // about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
textId.let(binding.messageStatusTextView::setText) messageStatus.messageText?.let(binding.messageStatusTextView::setText)
iconColor?.let(binding.messageStatusTextView::setTextColor) messageStatus.iconTint?.let(binding.messageStatusTextView::setTextColor)
iconID?.let { ContextCompat.getDrawable(context, it) } messageStatus.iconId?.let { ContextCompat.getDrawable(context, it) }
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this } ?.run { messageStatus.iconTint?.let { mutate().apply { setTint(it) } } ?: this }
?.let(binding.messageStatusImageView::setImageDrawable) ?.let(binding.messageStatusImageView::setImageDrawable)
// Potential options at this point are that the message is: // Potential options at this point are that the message is:
@ -380,7 +380,7 @@ class VisibleMessageView : FrameLayout {
@ColorInt val iconTint: Int?, @ColorInt val iconTint: Int?,
@StringRes val messageText: Int?) @StringRes val messageText: Int?)
private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when { private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo? = when {
message.isFailed -> message.isFailed ->
MessageStatusInfo(R.drawable.ic_delivery_status_failed, MessageStatusInfo(R.drawable.ic_delivery_status_failed,
getThemedColor(context, R.attr.danger), getThemedColor(context, R.attr.danger),
@ -409,7 +409,7 @@ class VisibleMessageView : FrameLayout {
) )
} }
} }
message.isSyncing || message.isResyncing -> message.isResyncing ->
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sending, R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
@ -421,16 +421,21 @@ class VisibleMessageView : FrameLayout {
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.read R.string.read
) )
message.isSent -> message.isSyncing || message.isSent -> // syncing should happen silently in the bg so we can mark it as sent
MessageStatusInfo( MessageStatusInfo(
R.drawable.ic_delivery_status_sent, R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color), context.getColorFromAttr(R.attr.message_status_color),
R.string.disappearingMessagesSent R.string.disappearingMessagesSent
) )
// deleted messages do not have a status but we care about styling them so they need to return something
message.isDeleted ->
MessageStatusInfo(null, null, null)
else -> { else -> {
// The message isn't one we care about for message statuses we display to the user (i.e., // The message isn't one we care about for message statuses we display to the user (i.e.,
// control messages etc. - see the `DisplayRecord.is<WHATEVER>` suite of methods for options). // control messages etc. - see the `DisplayRecord.is<WHATEVER>` suite of methods for options).
MessageStatusInfo(null, null, null) null
} }
} }
@ -479,10 +484,13 @@ class VisibleMessageView : FrameLayout {
// region Interaction // region Interaction
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
if (onPress == null || onSwipeToReply == null || onLongPress == null) { return false } if (onPress == null && onSwipeToReply == null && onLongPress == null) { return false }
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> onDown(event) MotionEvent.ACTION_DOWN -> onDown(event)
MotionEvent.ACTION_MOVE -> onMove(event) MotionEvent.ACTION_MOVE -> {
// only bother with movements if we have swipe to reply
onSwipeToReply?.let { onMove(event) }
}
MotionEvent.ACTION_CANCEL -> onCancel(event) MotionEvent.ACTION_CANCEL -> onCancel(event)
MotionEvent.ACTION_UP -> onUp(event) MotionEvent.ACTION_UP -> onUp(event)
} }

View File

@ -45,7 +45,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markUnidentified(long messageId, boolean unidentified); public abstract void markUnidentified(long messageId, boolean unidentified);
public abstract void markAsDeleted(long messageId); public abstract void markAsDeleted(long messageId, boolean isOutgoing, String displayedMessage);
public abstract boolean deleteMessage(long messageId); public abstract boolean deleteMessage(long messageId);
public abstract boolean deleteMessages(long[] messageId, long threadId); public abstract boolean deleteMessages(long[] messageId, long threadId);

View File

@ -320,18 +320,21 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
} }
override fun markAsDeleted(messageId: Long) { override fun markAsDeleted(messageId: Long, isOutgoing: Boolean, displayedMessage: String) {
val database = databaseHelper.writableDatabase val database = databaseHelper.writableDatabase
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(READ, 1) contentValues.put(READ, 1)
contentValues.put(BODY, "") contentValues.put(BODY, displayedMessage)
contentValues.put(HAS_MENTION, 0) contentValues.put(HAS_MENTION, 0)
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString())) database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
val attachmentDatabase = get(context).attachmentDatabase() val attachmentDatabase = get(context).attachmentDatabase()
queue { attachmentDatabase.deleteAttachmentsForMessage(messageId) } queue { attachmentDatabase.deleteAttachmentsForMessage(messageId) }
val threadId = getThreadIdForMessage(messageId) val threadId = getThreadIdForMessage(messageId)
markAs(messageId, MmsSmsColumns.Types.BASE_DELETED_TYPE, threadId) val deletedType = if (isOutgoing) { MmsSmsColumns.Types.BASE_DELETED_OUTGOING_TYPE} else {
MmsSmsColumns.Types.BASE_DELETED_INCOMING_TYPE
}
markAs(messageId, deletedType, threadId)
} }
override fun markExpireStarted(messageId: Long, startedTimestamp: Long) { override fun markExpireStarted(messageId: Long, startedTimestamp: Long) {

View File

@ -42,6 +42,7 @@ public interface MmsSmsColumns {
protected static final long JOINED_TYPE = 4; protected static final long JOINED_TYPE = 4;
protected static final long FIRST_MISSED_CALL_TYPE = 5; protected static final long FIRST_MISSED_CALL_TYPE = 5;
protected static final long BASE_DELETED_INCOMING_TYPE = 19;
protected static final long BASE_INBOX_TYPE = 20; protected static final long BASE_INBOX_TYPE = 20;
protected static final long BASE_OUTBOX_TYPE = 21; protected static final long BASE_OUTBOX_TYPE = 21;
protected static final long BASE_SENDING_TYPE = 22; protected static final long BASE_SENDING_TYPE = 22;
@ -50,7 +51,7 @@ public interface MmsSmsColumns {
protected static final long BASE_PENDING_SECURE_SMS_FALLBACK = 25; protected static final long BASE_PENDING_SECURE_SMS_FALLBACK = 25;
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26; protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
public static final long BASE_DRAFT_TYPE = 27; public static final long BASE_DRAFT_TYPE = 27;
protected static final long BASE_DELETED_TYPE = 28; protected static final long BASE_DELETED_OUTGOING_TYPE = 28;
protected static final long BASE_SYNCING_TYPE = 29; protected static final long BASE_SYNCING_TYPE = 29;
protected static final long BASE_RESYNCING_TYPE = 30; protected static final long BASE_RESYNCING_TYPE = 30;
protected static final long BASE_SYNC_FAILED_TYPE = 31; protected static final long BASE_SYNC_FAILED_TYPE = 31;
@ -61,6 +62,7 @@ public interface MmsSmsColumns {
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE, BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
BASE_PENDING_SECURE_SMS_FALLBACK, BASE_PENDING_SECURE_SMS_FALLBACK,
BASE_PENDING_INSECURE_SMS_FALLBACK, BASE_PENDING_INSECURE_SMS_FALLBACK,
BASE_DELETED_OUTGOING_TYPE,
OUTGOING_CALL_TYPE}; OUTGOING_CALL_TYPE};
@ -182,7 +184,9 @@ public interface MmsSmsColumns {
return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE; return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE;
} }
public static boolean isDeletedMessage(long type) { return (type & BASE_TYPE_MASK) == BASE_DELETED_TYPE; } public static boolean isDeletedMessage(long type) {
return (type & BASE_TYPE_MASK) == BASE_DELETED_OUTGOING_TYPE || (type & BASE_TYPE_MASK) == BASE_DELETED_INCOMING_TYPE;
}
public static boolean isJoinedType(long type) { public static boolean isJoinedType(long type) {
return (type & BASE_TYPE_MASK) == JOINED_TYPE; return (type & BASE_TYPE_MASK) == JOINED_TYPE;

View File

@ -159,7 +159,7 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
) )
} }
private fun deleteReactions(messageId: MessageId, query: String, args: Array<String>, notifyUnread: Boolean) { private fun deleteReactions(messageId: MessageId, query: String, args: Array<String>, notifyUnread: Boolean) {
writableDatabase.beginTransaction() writableDatabase.beginTransaction()
try { try {
writableDatabase.delete(TABLE_NAME, query, args) writableDatabase.delete(TABLE_NAME, query, args)
@ -174,7 +174,54 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
} finally { } finally {
writableDatabase.endTransaction() writableDatabase.endTransaction()
} }
} }
fun deleteMessageReactions(messageIds: List<MessageId>) {
if (messageIds.isEmpty()) return // Early exit if the list is empty
val conditions = mutableListOf<String>()
val args = mutableListOf<String>()
for (messageId in messageIds) {
conditions.add("($MESSAGE_ID = ? AND $IS_MMS = ?)")
args.add(messageId.id.toString())
args.add(if (messageId.mms) "1" else "0")
}
val query = conditions.joinToString(" OR ")
deleteReactions(
messageIds = messageIds,
query = query,
args = args.toTypedArray(),
notifyUnread = false
)
}
private fun deleteReactions(messageIds: List<MessageId>, query: String, args: Array<String>, notifyUnread: Boolean) {
writableDatabase.beginTransaction()
try {
writableDatabase.delete(TABLE_NAME, query, args)
// Update unread status for each message
for (messageId in messageIds) {
val hasReaction = hasReactions(messageId)
if (messageId.mms) {
DatabaseComponent.get(context).mmsDatabase().updateReactionsUnread(
writableDatabase, messageId.id, hasReaction, true, notifyUnread
)
} else {
DatabaseComponent.get(context).smsDatabase().updateReactionsUnread(
writableDatabase, messageId.id, hasReaction, true, notifyUnread
)
}
}
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
private fun hasReactions(messageId: MessageId): Boolean { private fun hasReactions(messageId: MessageId): Boolean {
val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" val query = "$MESSAGE_ID = ? AND $IS_MMS = ?"

View File

@ -237,14 +237,17 @@ public class SmsDatabase extends MessagingDatabase {
} }
@Override @Override
public void markAsDeleted(long messageId) { public void markAsDeleted(long messageId, boolean isOutgoing, String displayedMessage) {
SQLiteDatabase database = databaseHelper.getWritableDatabase(); SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1); contentValues.put(READ, 1);
contentValues.put(BODY, ""); contentValues.put(BODY, displayedMessage);
contentValues.put(HAS_MENTION, 0); contentValues.put(HAS_MENTION, 0);
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)}); database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK,
isOutgoing? MmsSmsColumns.Types.BASE_DELETED_OUTGOING_TYPE : MmsSmsColumns.Types.BASE_DELETED_INCOMING_TYPE
);
} }
@Override @Override

View File

@ -65,6 +65,9 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.getClosedGroup import org.session.libsession.utilities.getClosedGroup
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.DisappearingState import org.session.libsession.utilities.recipients.Recipient.DisappearingState
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsession.utilities.recipients.getType
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
@ -658,6 +661,12 @@ open class Storage @Inject constructor(
return database.getMessageFor(timestamp, address)?.run { getId() to isMms } return database.getMessageFor(timestamp, address)?.run { getId() to isMms }
} }
override fun getMessageType(timestamp: Long, author: String): MessageType? {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = fromSerialized(author)
return database.getMessageFor(timestamp, address)?.individualRecipient?.getType()
}
override fun updateSentTimestamp( override fun updateSentTimestamp(
messageID: Long, messageID: Long,
isMms: Boolean, isMms: Boolean,
@ -1807,6 +1816,12 @@ open class Storage @Inject constructor(
reactionDatabase.deleteMessageReactions(MessageId(messageId, mms)) reactionDatabase.deleteMessageReactions(MessageId(messageId, mms))
} }
override fun deleteReactions(messageIds: List<Long>, mms: Boolean) {
DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(
messageIds.map { MessageId(it, mms) }
)
}
override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) { override fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean) {
val recipientDb = recipientDatabase val recipientDb = recipientDatabase
recipientDb.setBlocked(recipients, isBlocked) recipientDb.setBlocked(recipients, isBlocked)

View File

@ -543,7 +543,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} else { } else {
showMuteDialog(this) { until -> showMuteDialog(this) { until ->
lifecycleScope.launch(Dispatchers.Default) { lifecycleScope.launch(Dispatchers.Default) {
Log.d("", "**** until: $until")
recipientDatabase.setMuted(thread.recipient, until) recipientDatabase.setMuted(thread.recipient, until)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.recyclerView.adapter!!.notifyDataSetChanged() binding.recyclerView.adapter!!.notifyDataSetChanged()

View File

@ -1,7 +1,9 @@
package org.thoughtcrime.securesms.notifications package org.thoughtcrime.securesms.notifications
import android.Manifest
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat.getString import androidx.core.content.ContextCompat.getString
@ -38,39 +40,52 @@ class PushReceiver @Inject constructor(
) { ) {
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
fun onPush(dataMap: Map<String, String>?) { /**
val result = dataMap?.decodeAndDecrypt() * Both push services should hit this method once they receive notification data
val data = result?.first * As long as it is properly formatted
if (data == null) { */
onPush() fun onPushDataReceived(dataMap: Map<String, String>?) {
addMessageReceiveJob(dataMap?.asPushData())
}
/**
* This is a fallback method in case the Huawei data is malformated,
* but it shouldn't happen. Old code used to send different data so this is kept as a safety
*/
fun onPushDataReceived(data: ByteArray?) {
addMessageReceiveJob(PushData(data = data, metadata = null))
}
private fun addMessageReceiveJob(pushData: PushData?){
// send a generic notification if we have no data
if (pushData?.data == null) {
sendGenericNotification()
return return
} }
handlePushData(data = data, metadata = result.second)
}
private fun handlePushData(data: ByteArray, metadata: PushNotificationMetadata?) {
try { try {
val params = when { val params = when {
metadata?.namespace == Namespace.CLOSED_GROUP_MESSAGES() -> { pushData.metadata?.namespace == Namespace.CLOSED_GROUP_MESSAGES() -> {
val groupId = AccountId(requireNotNull(metadata.account) { val groupId = AccountId(requireNotNull(pushData.metadata.account) {
"Received a closed group message push notification without an account ID" "Received a closed group message push notification without an account ID"
}) })
val envelop = checkNotNull(tryDecryptGroupMessage(groupId, data)) { val envelop = checkNotNull(tryDecryptGroupMessage(groupId, pushData.data)) {
"Unable to decrypt closed group message" "Unable to decrypt closed group message"
} }
MessageReceiveParameters( MessageReceiveParameters(
data = envelop.toByteArray(), data = envelop.toByteArray(),
serverHash = metadata.msg_hash, serverHash = pushData.metadata.msg_hash,
closedGroup = Destination.ClosedGroup(groupId.hexString) closedGroup = Destination.ClosedGroup(groupId.hexString)
) )
} }
metadata?.namespace == 0 || metadata == null -> { pushData.metadata?.namespace == 0 || pushData.metadata == null -> {
val envelopeAsData = MessageWrapper.unwrap(pushData.data).toByteArray()
MessageReceiveParameters( MessageReceiveParameters(
data = MessageWrapper.unwrap(data).toByteArray(), data = envelopeAsData,
serverHash = pushData.metadata?.msg_hash
) )
} }
@ -84,7 +99,9 @@ class PushReceiver @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "Failed to unwrap data for message due to error.", e) Log.d(TAG, "Failed to unwrap data for message due to error.", e)
} }
} }
private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? { private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? {
val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { it.groupKeys.decrypt(data) }) { val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { it.groupKeys.decrypt(data) }) {
@ -98,8 +115,18 @@ class PushReceiver @Inject constructor(
.build() .build()
} }
private fun onPush() { private fun sendGenericNotification() {
Log.d(TAG, "Failed to decode data for message.") Log.d(TAG, "Failed to decode data for message.")
// no need to do anything if notification permissions are not granted
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER) val builder = NotificationCompat.Builder(context, NotificationChannels.OTHER)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setColor(context.getColor(R.color.textsecure_primary)) .setColor(context.getColor(R.color.textsecure_primary))
@ -111,12 +138,10 @@ class PushReceiver @Inject constructor(
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true) .setAutoCancel(true)
if (context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { NotificationManagerCompat.from(context).notify(11111, builder.build())
NotificationManagerCompat.from(context).notify(11111, builder.build())
}
} }
private fun Map<String, String>.decodeAndDecrypt() = private fun Map<String, String>.asPushData(): PushData =
when { when {
// this is a v2 push notification // this is a v2 push notification
containsKey("spns") -> { containsKey("spns") -> {
@ -124,14 +149,14 @@ class PushReceiver @Inject constructor(
decrypt(Base64.decode(this["enc_payload"])) decrypt(Base64.decode(this["enc_payload"]))
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Invalid push notification", e) Log.e(TAG, "Invalid push notification", e)
null PushData(null, null)
} }
} }
// old v1 push notification; we still need this for receiving legacy closed group notifications // old v1 push notification; we still need this for receiving legacy closed group notifications
else -> this["ENCRYPTED_DATA"]?.let { Base64.decode(it) to null } else -> PushData(this["ENCRYPTED_DATA"]?.let(Base64::decode), null)
} }
private fun decrypt(encPayload: ByteArray): Pair<ByteArray?, PushNotificationMetadata?> { private fun decrypt(encPayload: ByteArray): PushData {
Log.d(TAG, "decrypt() called") Log.d(TAG, "decrypt() called")
val encKey = getOrCreateNotificationKey() val encKey = getOrCreateNotificationKey()
@ -149,11 +174,14 @@ class PushReceiver @Inject constructor(
val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata") val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata")
val metadata: PushNotificationMetadata = json.decodeFromString(String(metadataJson)) val metadata: PushNotificationMetadata = json.decodeFromString(String(metadataJson))
return (expectedList.getOrNull(1) as? BencodeString)?.value.also { return PushData(
// null content is valid only if we got a "data_too_long" flag data = (expectedList.getOrNull(1) as? BencodeString)?.value,
it?.let { check(metadata.data_len == it.size) { "wrong message data size" } } metadata = metadata
).also { pushData ->
// null data content is valid only if we got a "data_too_long" flag
pushData.data?.let { check(metadata.data_len == it.size) { "wrong message data size" } }
?: check(metadata.data_too_long) { "missing message data, but no too-long flag" } ?: check(metadata.data_too_long) { "missing message data, but no too-long flag" }
} to metadata }
} }
fun getOrCreateNotificationKey(): Key { fun getOrCreateNotificationKey(): Key {
@ -167,4 +195,9 @@ class PushReceiver @Inject constructor(
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString) IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
return key return key
} }
data class PushData(
val data: ByteArray?,
val metadata: PushNotificationMetadata?
)
} }

View File

@ -115,7 +115,7 @@ class ShareLogsDialog(private val updateCallback: (Boolean)->Unit): DialogFragme
} catch (e: Exception) { } catch (e: Exception) {
withContext(Main) { withContext(Main) {
Log.e("Loki", "Error saving logs", e) Log.e("Loki", "Error saving logs", e)
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show() Toast.makeText(context,getString(R.string.errorUnknown), Toast.LENGTH_LONG).show()
} }
} }
}.also { shareJob -> }.also { shareJob ->

View File

@ -14,6 +14,7 @@ import org.session.libsession.database.MessageDataProvider
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.groups.GroupManagerV2 import org.session.libsession.messaging.groups.GroupManagerV2
import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.MarkAsDeletedMessage
import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.control.MessageRequestResponse
import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
@ -45,6 +46,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
interface ConversationRepository { interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient? fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
@ -56,13 +59,29 @@ interface ConversationRepository {
fun clearDrafts(threadId: Long) fun clearDrafts(threadId: Long)
fun inviteContacts(threadId: Long, contacts: List<Recipient>) fun inviteContacts(threadId: Long, contacts: List<Recipient>)
fun setBlocked(threadId: Long, recipient: Recipient, blocked: Boolean) fun setBlocked(threadId: Long, recipient: Recipient, blocked: Boolean)
fun deleteLocally(recipient: Recipient, message: MessageRecord) fun markAsDeletedLocally(messages: Set<MessageRecord>, displayedMessage: String)
fun deleteMessages(messages: Set<MessageRecord>, threadId: Long)
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
fun setApproved(recipient: Recipient, isApproved: Boolean) fun setApproved(recipient: Recipient, isApproved: Boolean)
fun isKicked(recipient: Recipient): Boolean fun isKicked(recipient: Recipient): Boolean
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): Result<Unit> suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set<MessageRecord>)
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): Result<Unit> suspend fun delete1on1MessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
)
suspend fun deleteNoteToSelfMessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
)
suspend fun deleteLegacyGroupMessagesRemotely(
recipient: Recipient,
messages: Set<MessageRecord>
)
fun buildUnsendRequest(recipient: Recipient, message: MessageRecord): UnsendRequest?
suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit> suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun deleteThread(threadId: Long): Result<Unit> suspend fun deleteThread(threadId: Long): Result<Unit>
@ -174,14 +193,57 @@ class DefaultConversationRepository @Inject constructor(
} }
} }
override fun deleteLocally(recipient: Recipient, message: MessageRecord) { /**
if (shouldSendUnsendRequest(recipient)) { * This will delete these messages from the db
textSecurePreferences.getLocalNumber()?.let { * Not to be confused with 'marking messages as deleted'
MessageSender.send(buildUnsendRequest(message), Address.fromSerialized(it)) */
} override fun deleteMessages(messages: Set<MessageRecord>, threadId: Long) {
// split the messages into mms and sms
val (mms, sms) = messages.partition { it.isMms }
if(mms.isNotEmpty()){
messageDataProvider.deleteMessages(mms.map { it.id }, threadId, isSms = false)
} }
messageDataProvider.deleteMessage(message.id, !message.isMms) if(sms.isNotEmpty()){
messageDataProvider.deleteMessages(sms.map { it.id }, threadId, isSms = true)
}
}
/**
* This will mark the messages as deleted.
* They won't be removed from the db but instead will appear as a special type
* of message that says something like "This message was deleted"
*/
override fun markAsDeletedLocally(messages: Set<MessageRecord>, displayedMessage: String) {
// split the messages into mms and sms
val (mms, sms) = messages.partition { it.isMms }
if(mms.isNotEmpty()){
messageDataProvider.markMessagesAsDeleted(mms.map { MarkAsDeletedMessage(
messageId = it.id,
isOutgoing = it.isOutgoing
) },
isSms = false,
displayedMessage = displayedMessage
)
// delete reactions
storage.deleteReactions(messageIds = mms.map { it.id }, mms = true)
}
if(sms.isNotEmpty()){
messageDataProvider.markMessagesAsDeleted(sms.map { MarkAsDeletedMessage(
messageId = it.id,
isOutgoing = it.isOutgoing
) },
isSms = true,
displayedMessage = displayedMessage
)
// delete reactions
storage.deleteReactions(messageIds = sms.map { it.id }, mms = false)
}
} }
override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) { override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
@ -197,74 +259,82 @@ class DefaultConversationRepository @Inject constructor(
storage.setRecipientApproved(recipient, isApproved) storage.setRecipientApproved(recipient, isApproved)
} }
override suspend fun deleteForEveryone( override suspend fun deleteCommunityMessagesRemotely(
threadId: Long,
messages: Set<MessageRecord>
) {
val community = checkNotNull(lokiThreadDb.getOpenGroupChat(threadId)) { "Not a community" }
messages.forEach { message ->
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, community.room, community.server).await()
}
}
}
override suspend fun delete1on1MessagesRemotely(
threadId: Long, threadId: Long,
recipient: Recipient, recipient: Recipient,
message: MessageRecord messages: Set<MessageRecord>
): Result<Unit> { ) {
return runCatching { // delete the messages remotely
withContext(Dispatchers.Default) { val publicKey = recipient.address.serialize()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
if (openGroup != null) {
val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)
if (serverId != null) {
OpenGroupApi.deleteMessage(
serverID = serverId,
room = openGroup.room,
server = openGroup.server
).await()
messageDataProvider.deleteMessage(message.id, !message.isMms)
} else {
// If the server ID is null then this message is stuck in limbo (it has likely been
// deleted remotely but that deletion did not occur locally) - so we'll delete the
// message locally to clean up.
Log.w(
"ConversationRepository",
"Found community message without a server ID - deleting locally."
)
// Caution: The bool returned from `deleteMessage` is NOT "Was the message messages.forEach { message ->
// successfully deleted?" - it is "Was the thread itself also deleted because // delete from swarm
// removing that message resulted in an empty thread?". messageDataProvider.getServerHashForMessage(message.id, message.isMms)
if (message.isMms) { ?.let { serverHash ->
mmsDb.deleteMessage(message.id) SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
} else {
smsDb.deleteMessage(message.id)
}
}
} else // If this thread is NOT in a Community
{
val serverHash =
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
if (serverHash != null) {
var publicKey = recipient.address.serialize()
if (recipient.isLegacyClosedGroupRecipient) {
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
}
if (recipient.isClosedGroupV2Recipient) {
// admin check internally, assume either admin or all belong to user
groupManager.requestMessageDeletion(
groupId = AccountId(publicKey),
messageHashes = listOf(serverHash)
)
} else {
SnodeAPI.deleteMessage(
publicKey = publicKey,
swarmAuth = storage.userAuth!!,
serverHashes = listOf(serverHash)
).await()
}
if (shouldSendUnsendRequest(recipient)) {
MessageSender.send(
message = buildUnsendRequest(message),
address = recipient.address,
)
}
}
messageDataProvider.deleteMessage(message.id, !message.isMms)
} }
// send an UnsendRequest to user's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
userAddress?.let { MessageSender.send(unsendRequest, it) }
}
// send an UnsendRequest to recipient's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
}
}
override suspend fun deleteLegacyGroupMessagesRemotely(
recipient: Recipient,
messages: Set<MessageRecord>
) {
if (recipient.isClosedGroupRecipient) {
val publicKey = recipient.address
messages.forEach { message ->
// send an UnsendRequest to group's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, publicKey)
}
}
}
}
override suspend fun deleteNoteToSelfMessagesRemotely(
threadId: Long,
recipient: Recipient,
messages: Set<MessageRecord>
) {
// delete the messages remotely
val publicKey = recipient.address.serialize()
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
messages.forEach { message ->
// delete from swarm
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
?.let { serverHash ->
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
}
// send an UnsendRequest to user's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
userAddress?.let { MessageSender.send(unsendRequest, it) }
} }
} }
} }
@ -280,35 +350,6 @@ class DefaultConversationRepository @Inject constructor(
) )
} }
override suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long,
messages: Set<MessageRecord>
): Result<Unit> = kotlin.runCatching {
withContext(Dispatchers.Default) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
for (message in messages) {
val messageServerID =
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
messageServerIDs[messageServerID] = message
}
messageServerIDs.forEach { (messageServerID, message) ->
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server).await()
messageDataProvider.deleteMessage(message.id, !message.isMms)
}
} else {
for (message in messages) {
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
}
}
}
}
}
override suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> = runCatching { override suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> = runCatching {
val accountID = recipient.address.toString() val accountID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!

View File

@ -29,6 +29,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType import org.thoughtcrime.securesms.conversation.disappearingmessages.ExpiryType
@ -116,16 +117,20 @@ private fun RadioButtonIndicator(
@Composable @Composable
fun <T> TitledRadioButton( fun <T> TitledRadioButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(
horizontal = LocalDimensions.current.spacing,
vertical = LocalDimensions.current.smallSpacing
),
option: RadioOption<T>, option: RadioOption<T>,
onClick: () -> Unit onClick: () -> Unit
) { ) {
RadioButton( RadioButton(
modifier = modifier.heightIn(min = 60.dp) modifier = modifier
.contentDescription(option.contentDescription), .contentDescription(option.contentDescription),
onClick = onClick, onClick = onClick,
selected = option.selected, selected = option.selected,
enabled = option.enabled, enabled = option.enabled,
contentPadding = PaddingValues(horizontal = LocalDimensions.current.spacing), contentPadding = contentPadding,
content = { content = {
Column( Column(
modifier = Modifier modifier = Modifier

View File

@ -21,6 +21,7 @@ interface ThemeColors {
val warning: Color val warning: Color
val textAlert: Color val textAlert: Color
val danger: Color val danger: Color
val warning: Color
val disabled: Color val disabled: Color
val background: Color val background: Color
val backgroundSecondary: Color val backgroundSecondary: Color
@ -102,7 +103,7 @@ fun dangerButtonColors() = ButtonDefaults.buttonColors(
data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors { data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors {
override val isLight = false override val isLight = false
override val danger = dangerDark override val danger = dangerDark
override val warning = warningUniversal override val warning = primaryOrange
override val disabled = disabledDark override val disabled = disabledDark
override val background = classicDark0 override val background = classicDark0
override val backgroundSecondary = classicDark1 override val backgroundSecondary = classicDark1
@ -122,7 +123,7 @@ data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors
data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColors { data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColors {
override val isLight = true override val isLight = true
override val danger = dangerLight override val danger = dangerLight
override val warning = warningUniversal override val warning = primaryOrange
override val disabled = disabledLight override val disabled = disabledLight
override val background = classicLight6 override val background = classicLight6
override val backgroundSecondary = classicLight5 override val backgroundSecondary = classicLight5
@ -142,7 +143,7 @@ data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColor
data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors { data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
override val isLight = false override val isLight = false
override val danger = dangerDark override val danger = dangerDark
override val warning = warningUniversal override val warning = primaryOrange
override val disabled = disabledDark override val disabled = disabledDark
override val background = oceanDark2 override val background = oceanDark2
override val backgroundSecondary = oceanDark1 override val backgroundSecondary = oceanDark1
@ -162,7 +163,7 @@ data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
data class OceanLight(override val primary: Color = primaryBlue) : ThemeColors { data class OceanLight(override val primary: Color = primaryBlue) : ThemeColors {
override val isLight = true override val isLight = true
override val danger = dangerLight override val danger = dangerLight
override val warning = warningUniversal override val warning = primaryOrange
override val disabled = disabledLight override val disabled = disabledLight
override val background = oceanLight7 override val background = oceanLight7
override val backgroundSecondary = oceanLight6 override val backgroundSecondary = oceanLight6

View File

@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?colorAccent">
<path
android:fillColor="@android:color/white"
android:pathData="m12,2.4355c-5.2796,0 -9.5645,4.2848 -9.5645,9.5645 0,5.2796 4.2848,9.5645 9.5645,9.5645 5.2796,0 9.5645,-4.2848 9.5645,-9.5645 0,-5.2796 -4.2848,-9.5645 -9.5645,-9.5645zM12.123,7.9375 L15.6777,11.4922 14.9707,12.1992 12.623,9.8515v6.1797h-1v-6.1797l-1.9961,1.9941 -0.3535,0.3535 -0.707,-0.707 0.3535,-0.3535 3.2031,-3.2012z"
android:strokeWidth=".95645"/>
</vector>

View File

@ -2,11 +2,12 @@
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="50" android:viewportWidth="50"
android:viewportHeight="50"> android:viewportHeight="50"
android:tint="?attr/colorControlNormal">
<path <path
android:pathData="M14.295,11.247H18.146V6.687C18.146,4.876 19.089,3.851 20.999,3.851H29.237V13.259C29.237,15.691 30.518,16.956 32.935,16.956H41.606V33.231C41.606,35.059 40.648,36.068 38.738,36.068H35.057V39.918H39.074C43.272,39.918 45.458,37.697 45.458,33.471V17.892C45.458,15.308 44.92,13.658 43.368,12.067L33.565,2.093C32.096,0.587 30.328,0 28.065,0H20.679C16.481,0 14.295,2.218 14.295,6.448V11.247ZM32.452,12.773V5.46L40.604,13.741H33.404C32.73,13.741 32.452,13.447 32.452,12.773Z" android:pathData="M14.295,11.247H18.146V6.687C18.146,4.876 19.089,3.851 20.999,3.851H29.237V13.259C29.237,15.691 30.518,16.956 32.935,16.956H41.606V33.231C41.606,35.059 40.648,36.068 38.738,36.068H35.057V39.918H39.074C43.272,39.918 45.458,37.697 45.458,33.471V17.892C45.458,15.308 44.92,13.658 43.368,12.067L33.565,2.093C32.096,0.587 30.328,0 28.065,0H20.679C16.481,0 14.295,2.218 14.295,6.448V11.247ZM32.452,12.773V5.46L40.604,13.741H33.404C32.73,13.741 32.452,13.447 32.452,12.773Z"
android:fillColor="#000000"/> android:fillColor="#FFFFFF"/>
<path <path
android:pathData="M4.571,43.552C4.571,47.798 6.744,50 10.955,50H29.353C33.563,50 35.737,47.779 35.737,43.552V28.424C35.737,25.791 35.403,24.559 33.756,22.88L23.103,12.062C21.52,10.448 20.172,10.082 17.805,10.082H10.955C6.76,10.082 4.571,12.283 4.571,16.529V43.552ZM8.422,43.313V16.753C8.422,14.957 9.365,13.932 11.278,13.932H17.318V24.693C17.318,27.509 18.711,28.882 21.491,28.882H31.882V43.313C31.882,45.14 30.923,46.149 29.03,46.149H11.262C9.365,46.149 8.422,45.14 8.422,43.313ZM21.872,25.486C21.061,25.486 20.715,25.143 20.715,24.328V14.688L31.347,25.486H21.872Z" android:pathData="M4.571,43.552C4.571,47.798 6.744,50 10.955,50H29.353C33.563,50 35.737,47.779 35.737,43.552V28.424C35.737,25.791 35.403,24.559 33.756,22.88L23.103,12.062C21.52,10.448 20.172,10.082 17.805,10.082H10.955C6.76,10.082 4.571,12.283 4.571,16.529V43.552ZM8.422,43.313V16.753C8.422,14.957 9.365,13.932 11.278,13.932H17.318V24.693C17.318,27.509 18.711,28.882 21.491,28.882H31.882V43.313C31.882,45.14 30.923,46.149 29.03,46.149H11.262C9.365,46.149 8.422,45.14 8.422,43.313ZM21.872,25.486C21.061,25.486 20.715,25.143 20.715,24.328V14.688L31.347,25.486H21.872Z"
android:fillColor="#000000"/> android:fillColor="#FFFFFF"/>
</vector> </vector>

View File

@ -10,4 +10,5 @@
android:strokeWidth="1" android:strokeWidth="1"
android:pathData="M13.567,13.713m-12.5,0a12.5,12.5 0,1 1,25 0a12.5,12.5 0,1 1,-25 0" android:pathData="M13.567,13.713m-12.5,0a12.5,12.5 0,1 1,25 0a12.5,12.5 0,1 1,-25 0"
android:strokeColor="?textColorAlert"/> android:strokeColor="?textColorAlert"/>
</vector> </vector>

View File

@ -8,4 +8,5 @@
android:pathData="M13.567,13.713m-12.5,0a12.5,12.5 0,1 1,25 0a12.5,12.5 0,1 1,-25 0" android:pathData="M13.567,13.713m-12.5,0a12.5,12.5 0,1 1,25 0a12.5,12.5 0,1 1,-25 0"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="?textColorAlert"/> android:strokeColor="?textColorAlert"/>
</vector> </vector>

View File

@ -386,4 +386,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:focusable="true"
android:clickable="true"
android:visibility="gone">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="8dp"
app:SpinKit_Color="@android:color/white" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -173,7 +173,8 @@
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset" android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset"
app:rippleColor="@color/button_primary_ripple" app:rippleColor="@color/button_primary_ripple"
android:src="@drawable/ic_plus" /> android:src="@drawable/ic_plus"
android:tint="?message_sent_text_color"/>
</RelativeLayout> </RelativeLayout>

View File

@ -118,10 +118,11 @@
android:id="@+id/mediasend_send_button" android:id="@+id/mediasend_send_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:scaleType="fitXY" android:scaleType="fitCenter"
android:padding="10dp"
android:contentDescription="@string/send" android:contentDescription="@string/send"
android:src="?conversation_transport_sms_indicator" android:src="@drawable/ic_arrow_up"
android:background="@drawable/circle_touch_highlight_background"/> android:background="@drawable/accent_dot"/>
</FrameLayout> </FrameLayout>

View File

@ -27,7 +27,7 @@
android:visibility="gone" android:visibility="gone"
app:tint="?android:textColorTertiary" app:tint="?android:textColorTertiary"
tools:src="@drawable/ic_timer" tools:src="@drawable/ic_timer"
tools:visibility="visible"/> tools:visibility="visible" />
<org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView <org.thoughtcrime.securesms.conversation.v2.components.ExpirationTimerView
android:id="@+id/expirationTimerView" android:id="@+id/expirationTimerView"
@ -37,47 +37,57 @@
android:visibility="gone" android:visibility="gone"
app:tint="?android:textColorTertiary" app:tint="?android:textColorTertiary"
tools:src="@drawable/ic_timer" tools:src="@drawable/ic_timer"
tools:visibility="visible"/> tools:visibility="visible" />
<TextView <LinearLayout
android:id="@+id/textView" android:id="@+id/controlContentView"
android:contentDescription="@string/AccessibilityId_control_message" android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:textColor="?android:textColorTertiary" android:orientation="vertical">
android:textSize="@dimen/very_small_font_size"
tools:text="You disabled disappearing messages" />
<FrameLayout
android:id="@+id/call_view"
style="@style/CallMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/call_text_view" android:id="@+id/textView"
android:textColor="?message_received_text_color"
android:textAlignment="center"
android:layout_gravity="center"
android:gravity="center"
tools:text="You missed a call"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
app:drawableStartCompat="@drawable/ic_missed_call" /> android:contentDescription="@string/AccessibilityId_control_message"
android:gravity="center"
android:textColor="?android:textColorTertiary"
android:textSize="@dimen/very_small_font_size"
tools:text="You disabled disappearing messages" />
</FrameLayout> <FrameLayout
android:id="@+id/call_view"
style="@style/CallMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/followSetting" android:id="@+id/call_text_view"
style="@style/Widget.Session.Button.Common.Borderless" android:layout_width="match_parent"
android:layout_marginTop="4dp" android:layout_height="match_parent"
android:textColor="@color/accent_green" android:layout_gravity="center"
android:textSize="@dimen/very_small_font_size" android:gravity="center"
android:text="@string/disappearingMessagesFollowSetting" android:textAlignment="center"
android:contentDescription="@string/AccessibilityId_disappearingMessagesFollowSetting" android:textColor="?message_received_text_color"
android:layout_width="match_parent" app:drawableStartCompat="@drawable/ic_missed_call"
android:layout_height="wrap_content"/> tools:text="You missed a call" />
</FrameLayout>
<TextView
android:id="@+id/followSetting"
style="@style/Widget.Session.Button.Common.Borderless"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="@null"
android:contentDescription="@string/AccessibilityId_disappearingMessagesFollowSetting"
android:text="@string/disappearingMessagesFollowSetting"
android:textColor="@color/accent_green"
android:textSize="@dimen/very_small_font_size" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -2,12 +2,11 @@
<org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView <org.thoughtcrime.securesms.conversation.v2.messages.DeletedMessageView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing">
android:gravity="center">
<ImageView <ImageView
android:id="@+id/deletedMessageViewIconImageView" android:id="@+id/deletedMessageViewIconImageView"

View File

@ -24,6 +24,7 @@
android:id="@+id/deletedMessageView" android:id="@+id/deletedMessageView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintHorizontal_bias="0"
/> />
<include layout="@layout/view_pending_attachment" <include layout="@layout/view_pending_attachment"

View File

@ -41,8 +41,6 @@
<attr name="conversation_editor_text_color" format="reference|color"/> <attr name="conversation_editor_text_color" format="reference|color"/>
<attr name="conversation_input_background" format="reference"/> <attr name="conversation_input_background" format="reference"/>
<attr name="conversation_input_inline_attach_icon_tint" format="reference"/> <attr name="conversation_input_inline_attach_icon_tint" format="reference"/>
<attr name="conversation_transport_sms_indicator" format="reference"/>
<attr name="conversation_transport_push_indicator" format="reference"/>
<attr name="conversation_transport_popup_background" format="reference"/> <attr name="conversation_transport_popup_background" format="reference"/>
<attr name="conversation_emoji_toggle" format="reference"/> <attr name="conversation_emoji_toggle" format="reference"/>
<attr name="conversation_sticker_toggle" format="reference"/> <attr name="conversation_sticker_toggle" format="reference"/>

View File

@ -164,7 +164,7 @@
<item name="android:background">@drawable/unimportant_dialog_text_button_background</item> <item name="android:background">@drawable/unimportant_dialog_text_button_background</item>
<item name="android:textStyle">bold</item> <item name="android:textStyle">bold</item>
</style> </style>
<style name="Widget.Session.Button.Dialog.DangerText"> <style name="Widget.Session.Button.Dialog.DangerText">
<item name="android:background">@drawable/danger_dialog_text_button_background</item> <item name="android:background">@drawable/danger_dialog_text_button_background</item>
<item name="android:textColor">?danger</item> <item name="android:textColor">?danger</item>

View File

@ -21,7 +21,6 @@
<item name="android:backgroundDimEnabled">true</item> <item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.6</item> <item name="android:backgroundDimAmount">0.6</item>
<item name="dialogCornerRadius">@dimen/dialog_corner_radius</item> <item name="dialogCornerRadius">@dimen/dialog_corner_radius</item>
<item name="conversation_transport_sms_indicator">@drawable/ic_arrow_up_circle_24</item>
<item name="android:alertDialogTheme">@style/ThemeOverlay.Session.AlertDialog</item> <item name="android:alertDialogTheme">@style/ThemeOverlay.Session.AlertDialog</item>
<item name="alertDialogTheme">@style/ThemeOverlay.Session.AlertDialog</item> <item name="alertDialogTheme">@style/ThemeOverlay.Session.AlertDialog</item>
<item name="conversationMenuSearchTintColor">?android:textColorPrimary</item> <item name="conversationMenuSearchTintColor">?android:textColorPrimary</item>
@ -173,8 +172,6 @@
<item name="conversation_editor_background">#22ffffff</item> <item name="conversation_editor_background">#22ffffff</item>
<item name="conversation_editor_text_color">#ffeeeeee</item> <item name="conversation_editor_text_color">#ffeeeeee</item>
<item name="conversation_input_inline_attach_icon_tint">@color/core_grey_05</item> <item name="conversation_input_inline_attach_icon_tint">@color/core_grey_05</item>
<item name="conversation_transport_sms_indicator">@drawable/ic_arrow_up_circle_24</item>
<item name="conversation_transport_push_indicator">@drawable/ic_arrow_up_circle_24</item>
<item name="conversation_transport_popup_background">@color/black</item> <item name="conversation_transport_popup_background">@color/black</item>
<item name="conversation_attach_camera">@drawable/ic_photo_camera_dark</item> <item name="conversation_attach_camera">@drawable/ic_photo_camera_dark</item>
<item name="conversation_attach_image">@drawable/ic_image_dark</item> <item name="conversation_attach_image">@drawable/ic_image_dark</item>

View File

@ -23,6 +23,6 @@ class FirebasePushService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) { override fun onMessageReceived(message: RemoteMessage) {
Log.d(TAG, "Received a push notification.") Log.d(TAG, "Received a push notification.")
pushReceiver.onPush(message.data) pushReceiver.onPushDataReceived(message.data)
} }
} }

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2 package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import com.goterl.lazysodium.utils.KeyPair import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -12,7 +13,6 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@ -27,6 +27,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private val repository = mock<ConversationRepository>() private val repository = mock<ConversationRepository>()
private val storage = mock<Storage>() private val storage = mock<Storage>()
private val application = mock<Application>()
private val threadId = 123L private val threadId = 123L
private val edKeyPair = mock<KeyPair>() private val edKeyPair = mock<KeyPair>()
@ -41,7 +42,10 @@ class ConversationViewModelTest: BaseViewModelTest() {
storage = storage, storage = storage,
messageDataProvider = mock(), messageDataProvider = mock(),
groupDb = mock(), groupDb = mock(),
threadDb = mock() threadDb = mock(),
textSecurePreferences = mock(),
lokiMessageDb = mock(),
application = mock(),
) )
} }
@ -94,59 +98,27 @@ class ConversationViewModelTest: BaseViewModelTest() {
verify(repository).setBlocked(threadId, recipient, false) verify(repository).setBlocked(threadId, recipient, false)
} }
@Test
fun `should delete locally`() {
val message = mock<MessageRecord>()
viewModel.deleteLocally(message)
verify(repository).deleteLocally(recipient, message)
}
@Test
fun `should emit error message on failure to delete a message for everyone`() = runBlockingTest {
val message = mock<MessageRecord>()
val error = Throwable()
whenever(repository.deleteForEveryone(anyLong(), any(), any()))
.thenReturn(Result.failure(error))
viewModel.deleteForEveryone(message)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test
fun `should emit error message on failure to delete messages without unsend request`() =
runBlockingTest {
val message = mock<MessageRecord>()
val error = Throwable()
whenever(repository.deleteMessageWithoutUnsendRequest(anyLong(), anySet()))
.thenReturn(Result.failure(error))
viewModel.deleteMessagesWithoutUnsendRequest(setOf(message))
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
}
@Test @Test
fun `should emit error message on ban user failure`() = runBlockingTest { fun `should emit error message on ban user failure`() = runBlockingTest {
val error = Throwable() val error = Throwable()
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.failure(error)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.failure(error))
whenever(application.getString(any())).thenReturn("Ban failed")
viewModel.banUser(recipient) viewModel.banUser(recipient)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error")) assertThat(viewModel.uiState.first().uiMessages.first().message, equalTo("Ban failed"))
} }
@Test @Test
fun `should emit a message on ban user success`() = runBlockingTest { fun `should emit a message on ban user success`() = runBlockingTest {
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banUser(recipient) viewModel.banUser(recipient)
assertThat( assertThat(
viewModel.uiState.first().uiMessages.first().message, viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user") equalTo("User banned")
) )
} }
@ -154,21 +126,23 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should emit error message on ban user and delete all failure`() = runBlockingTest { fun `should emit error message on ban user and delete all failure`() = runBlockingTest {
val error = Throwable() val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.failure(error)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.failure(error))
whenever(application.getString(any())).thenReturn("Ban failed")
viewModel.banAndDeleteAll(messageRecord) viewModel.banAndDeleteAll(messageRecord)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error")) assertThat(viewModel.uiState.first().uiMessages.first().message, equalTo("Ban failed"))
} }
@Test @Test
fun `should emit a message on ban user and delete all success`() = runBlockingTest { fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.success(Unit)) whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banAndDeleteAll(messageRecord) viewModel.banAndDeleteAll(messageRecord)
assertThat( assertThat(
viewModel.uiState.first().uiMessages.first().message, viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user and deleted all their messages") equalTo("User banned")
) )
} }
@ -176,6 +150,8 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should remove shown message`() = runBlockingTest { fun `should remove shown message`() = runBlockingTest {
// Given that a message is generated // Given that a message is generated
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit)) whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banUser(recipient) viewModel.banUser(recipient)
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1)) assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1))
// When the message is shown // When the message is shown

View File

@ -1,5 +1,6 @@
package org.session.libsession.database package org.session.libsession.database
import org.session.libsession.messaging.messages.MarkAsDeletedMessage
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
@ -23,8 +24,8 @@ interface MessageDataProvider {
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>> fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: Long, isSms: Boolean) fun deleteMessage(messageID: Long, isSms: Boolean)
fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean) fun deleteMessages(messageIDs: List<Long>, threadId: Long, isSms: Boolean)
fun updateMessageAsDeleted(timestamp: Long, author: String): Long? fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String)
fun updateMessageAsDeleted(messageId: Long, isSms: Boolean): Long fun markMessagesAsDeleted(messages: List<MarkAsDeletedMessage>, isSms: Boolean, displayedMessage: String)
fun getServerHashForMessage(messageID: Long, mms: Boolean): String? fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream? fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?

View File

@ -36,6 +36,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.Recipient.RecipientSettings import org.session.libsession.utilities.recipients.Recipient.RecipientSettings
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup import org.session.libsignal.messages.SignalServiceGroup
@ -124,6 +125,7 @@ interface StorageProtocol {
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long> fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment> fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
fun getMessageIdInDatabase(timestamp: Long, author: String): Pair<Long, Boolean>? // TODO: This is a weird name fun getMessageIdInDatabase(timestamp: Long, author: String): Pair<Long, Boolean>? // TODO: This is a weird name
fun getMessageType(timestamp: Long, author: String): MessageType?
fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long) fun updateSentTimestamp(messageID: Long, isMms: Boolean, openGroupSentTimestamp: Long, threadId: Long)
fun markAsResyncing(timestamp: Long, author: String) fun markAsResyncing(timestamp: Long, author: String)
fun markAsSyncing(timestamp: Long, author: String) fun markAsSyncing(timestamp: Long, author: String)
@ -254,6 +256,7 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean) fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean) fun deleteReactions(messageId: Long, mms: Boolean)
fun deleteReactions(messageIds: List<Long>, mms: Boolean)
fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false) fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
fun setRecipientHash(recipient: Recipient, recipientHash: String?) fun setRecipientHash(recipient: Recipient, recipientHash: String?)
fun blockedContacts(): List<Recipient> fun blockedContacts(): List<Recipient>

View File

@ -0,0 +1,6 @@
package org.session.libsession.messaging.messages
data class MarkAsDeletedMessage(
val messageId: Long,
val isOutgoing: Boolean
)

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.ExpiryMode import network.loki.messenger.libsession_util.util.ExpiryMode
import network.loki.messenger.libsession_util.util.Sodium import network.loki.messenger.libsession_util.util.Sodium
import org.session.libsession.R
import org.session.libsession.avatars.AvatarHelper import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.database.userAuth import org.session.libsession.database.userAuth
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
@ -50,6 +51,7 @@ import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.MessageType
import org.session.libsignal.crypto.ecc.DjbECPrivateKey import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair import org.session.libsignal.crypto.ecc.ECKeyPair
@ -258,23 +260,66 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? { fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey() val userPublicKey = MessagingModuleConfiguration.shared.storage.getUserPublicKey()
if (message.sender != message.author && (message.sender != userPublicKey && userPublicKey != null)) { return null }
val context = MessagingModuleConfiguration.shared.context
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
val userAuth = storage.userAuth ?: return null val userAuth = storage.userAuth ?: return null
val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key ->
var admin = false
val groupID = doubleEncodeGroupID(key)
val group = storage.getGroup(groupID)
if(group != null) {
admin = group.admins.map { it.toString() }.contains(message.sender)
}
admin
} ?: false
// First we need to determine the validity of the UnsendRequest
// It is valid if:
val requestIsValid = message.sender == message.author || // the sender is the author of the message
message.author == userPublicKey || // the sender is the current user
isLegacyGroupAdmin // sender is an admin of legacy group
if (!requestIsValid) { return null }
val context = MessagingModuleConfiguration.shared.context
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val timestamp = message.timestamp ?: return null val timestamp = message.timestamp ?: return null
val author = message.author ?: return null val author = message.author ?: return null
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash -> val messageType = storage.getMessageType(timestamp, author) ?: return null
SnodeAPI.deleteMessage(author, swarmAuth = userAuth, listOf(serverHash))
// send a /delete rquest for 1on1 messages
if(messageType == MessageType.ONE_ON_ONE) {
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
GlobalScope.launch(Dispatchers.IO) { // using GlobalScope as we are slowly migrating to coroutines but we can't migrate everything at once
try {
SnodeAPI.deleteMessage(author, listOf(serverHash))
} catch (e: Exception) {
}
}
}
} }
val deletedMessageId = messageDataProvider.updateMessageAsDeleted(timestamp, author)
// the message is marked as deleted locally
// except for 'note to self' where the message is completely deleted
if(messageType == MessageType.NOTE_TO_SELF){
messageDataProvider.deleteMessage(messageIdToDelete, !mms)
} else {
messageDataProvider.markMessageAsDeleted(
timestamp = timestamp,
author = author,
displayedMessage = context.getString(R.string.deleteMessageDeletedGlobally)
)
}
// delete reactions
storage.deleteReactions(messageId = messageIdToDelete, mms = mms)
// update notification
if (!messageDataProvider.isOutgoingMessage(timestamp)) { if (!messageDataProvider.isOutgoingMessage(timestamp)) {
SSKEnvironment.shared.notificationManager.updateNotification(context) SSKEnvironment.shared.notificationManager.updateNotification(context)
} }
return deletedMessageId return messageIdToDelete
} }
fun handleMessageRequestResponse(message: MessageRequestResponse) { fun handleMessageRequestResponse(message: MessageRequestResponse) {

View File

@ -18,6 +18,8 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select import kotlinx.coroutines.selects.select
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.coroutineScope
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.bind
@ -867,63 +869,70 @@ object SnodeAPI {
) )
} }
} }
@Suppress("UNCHECKED_CAST") suspend fun deleteMessage(publicKey: String, swarmAuth: SwarmAuth, serverHashes: List<String>) {
fun deleteMessage( retryWithUniformInterval {
publicKey: String, val snode = getSingleTargetSnode(publicKey).await()
swarmAuth: SwarmAuth, val params = buildAuthenticatedParameters(
serverHashes: List<String> auth = swarmAuth,
): Promise<Map<String, Boolean>, Exception> = scope.retrySuspendAsPromise(maxRetryCount) { namespace = null,
val params = buildAuthenticatedParameters( verificationData = { _, _ ->
auth = swarmAuth, buildString {
namespace = null, append(Snode.Method.DeleteMessage.rawValue)
verificationData = { _, _ -> serverHashes.forEach(this::append)
buildString { }
append(Snode.Method.DeleteMessage.rawValue)
serverHashes.forEach(this::append)
} }
) {
this["messages"] = serverHashes
} }
) { val rawResponse = invoke(
this["messages"] = serverHashes Snode.Method.DeleteMessage,
} snode,
params,
publicKey
).await()
val snode = getSingleTargetSnode(publicKey).await() // thie next step is to verify the nodes on our swarm and check that the message was deleted
val rawResponse = invoke(Snode.Method.DeleteMessage, snode, params, publicKey).await() // on at least one of them
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@retrySuspendAsPromise mapOf() val swarms = rawResponse["swarm"] as? Map<String, Any> ?: throw (Error.Generic)
swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
(rawJSON as? Map<String, Any>)?.let { json ->
val isFailed = json["failed"] as? Boolean ?: false
val statusCode = json["code"] as? String
val reason = json["reason"] as? String
if (isFailed) { val deletedMessages = swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) ->
Log.e( (rawJSON as? Map<String, Any>)?.let { json ->
"Loki", val isFailed = json["failed"] as? Boolean ?: false
"Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode)." val statusCode = json["code"] as? String
) val reason = json["reason"] as? String
false
} else { if (isFailed) {
// Hashes of deleted messages Log.e(
val hashes = json["deleted"] as List<String> "Loki",
val signature = json["signature"] as String "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode)."
val snodePublicKey = Key.fromHexString(hexSnodePublicKey) )
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) false
val message = sequenceOf(swarmAuth.accountId.hexString) } else {
// Hashes of deleted messages
val hashes = json["deleted"] as List<String>
val signature = json["signature"] as String
val snodePublicKey = Key.fromHexString(hexSnodePublicKey)
// The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
val message = sequenceOf(swarmAuth.accountId.hexString)
.plus(serverHashes) .plus(serverHashes)
.plus(hashes) .plus(hashes)
.toByteArray() .toByteArray()
sodium.cryptoSignVerifyDetached( sodium.cryptoSignVerifyDetached(
Base64.decode(signature), Base64.decode(signature),
message, message,
message.size, message.size,
snodePublicKey.asBytes snodePublicKey.asBytes
) )
}
} }
} }
// if all the nodes returned false (the message was not deleted) then we consider this a failed scenario
if (deletedMessages.entries.all { !it.value }) throw (Error.Generic)
} }
} }
// Parsing // Parsing
private fun parseSnodes(rawResponse: Any): List<Snode> = private fun parseSnodes(rawResponse: Any): List<Snode> =
(rawResponse as? Map<*, *>) (rawResponse as? Map<*, *>)

View File

@ -0,0 +1,14 @@
package org.session.libsession.utilities.recipients
enum class MessageType {
ONE_ON_ONE, LEGACY_GROUP, GROUPS_V2, NOTE_TO_SELF, COMMUNITY
}
fun Recipient.getType(): MessageType =
when{
isCommunityRecipient -> MessageType.COMMUNITY
isLocalNumber -> MessageType.NOTE_TO_SELF
isClosedGroupRecipient -> MessageType.LEGACY_GROUP //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
//isXXXXX -> RecipientType.GROUPS_V2 //todo GROUPS V2 this property will change for groups v2. Check for legacyGroup here
else -> MessageType.ONE_ON_ONE
}

View File

@ -1,93 +0,0 @@
package org.session.libsession.utilities.task;
import android.app.ProgressDialog;
import android.os.AsyncTask;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
public abstract class SnackbarAsyncTask<Params>
extends AsyncTask<Params, Void, Void>
implements View.OnClickListener
{
private final View view;
private final String snackbarText;
private final String snackbarActionText;
private final int snackbarActionColor;
private final int snackbarDuration;
private final boolean showProgress;
private @Nullable Params reversibleParameter;
private @Nullable ProgressDialog progressDialog;
public SnackbarAsyncTask(View view,
String snackbarText,
String snackbarActionText,
int snackbarActionColor,
int snackbarDuration,
boolean showProgress)
{
this.view = view;
this.snackbarText = snackbarText;
this.snackbarActionText = snackbarActionText;
this.snackbarActionColor = snackbarActionColor;
this.snackbarDuration = snackbarDuration;
this.showProgress = showProgress;
}
@Override
protected void onPreExecute() {
if (this.showProgress) this.progressDialog = ProgressDialog.show(view.getContext(), "", "", true);
else this.progressDialog = null;
}
@SafeVarargs
@Override
protected final Void doInBackground(Params... params) {
this.reversibleParameter = params != null && params.length > 0 ?params[0] : null;
executeAction(reversibleParameter);
return null;
}
@Override
protected void onPostExecute(Void result) {
if (this.showProgress && this.progressDialog != null) {
this.progressDialog.dismiss();
this.progressDialog = null;
}
Snackbar.make(view, snackbarText, snackbarDuration)
.setAction(snackbarActionText, this)
.setActionTextColor(snackbarActionColor)
.show();
}
@Override
public void onClick(View v) {
new AsyncTask<Void, Void, Void>() {
@Override
protected void onPreExecute() {
if (showProgress) progressDialog = ProgressDialog.show(view.getContext(), "", "", true);
else progressDialog = null;
}
@Override
protected Void doInBackground(Void... params) {
reverseAction(reversibleParameter);
return null;
}
@Override
protected void onPostExecute(Void result) {
if (showProgress && progressDialog != null) {
progressDialog.dismiss();
progressDialog = null;
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
protected abstract void executeAction(@Nullable Params parameter);
protected abstract void reverseAction(@Nullable Params parameter);
}

View File

@ -281,7 +281,6 @@
<item quantity="one">Skrap Boodskap</item> <item quantity="one">Skrap Boodskap</item>
<item quantity="other">Skrap Boodskappe</item> <item quantity="other">Skrap Boodskappe</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Is jy seker jy wil hierdie boodskap skrap?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Boodskap verwyder</item> <item quantity="one">Boodskap verwyder</item>
<item quantity="other">Boodskappe verwyder</item> <item quantity="other">Boodskappe verwyder</item>
@ -297,7 +296,6 @@
<item quantity="one">Kon nie boodskap uitvee nie</item> <item quantity="one">Kon nie boodskap uitvee nie</item>
<item quantity="other">Kon nie boodskappe uitvee nie</item> <item quantity="other">Kon nie boodskappe uitvee nie</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Is jy seker jy wil hierdie boodskappe skrap?</string>
<string name="deleteMessagesDescriptionDevice">Is jy seker jy wil hierdie boodskappe net van hierdie toestel verwyder?</string> <string name="deleteMessagesDescriptionDevice">Is jy seker jy wil hierdie boodskappe net van hierdie toestel verwyder?</string>
<string name="deleteMessagesDescriptionEveryone">Is jy seker jy wil hierdie boodskappe vir almal verwyder?</string> <string name="deleteMessagesDescriptionEveryone">Is jy seker jy wil hierdie boodskappe vir almal verwyder?</string>
<string name="deleting">Skrap...</string> <string name="deleting">Skrap...</string>

View File

@ -281,7 +281,6 @@
<item quantity="many">حذف الرسائل</item> <item quantity="many">حذف الرسائل</item>
<item quantity="other">حذف الرسائل</item> <item quantity="other">حذف الرسائل</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">هل أنت متأكد من أنك تريد حذف هذه الرسالة؟</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="zero">تم حذف الرسائل</item> <item quantity="zero">تم حذف الرسائل</item>
<item quantity="one">تم حذف الرسالة</item> <item quantity="one">تم حذف الرسالة</item>
@ -297,7 +296,6 @@
<string name="deleteMessageDeviceOnly">حذف على هذا الجهاز فقط</string> <string name="deleteMessageDeviceOnly">حذف على هذا الجهاز فقط</string>
<string name="deleteMessageDevicesAll">حذف على جميع أجهزتي</string> <string name="deleteMessageDevicesAll">حذف على جميع أجهزتي</string>
<string name="deleteMessageEveryone">حذف للجميع</string> <string name="deleteMessageEveryone">حذف للجميع</string>
<string name="deleteMessagesConfirm">هل أنت متأكد من أنك تريد حذف هذه الرسائل؟</string>
<string name="deleteMessagesDescriptionDevice">هل أنت متيقِّن من أنك تريد مسح هذه الرسائل من هذا الجهاز فقط؟</string> <string name="deleteMessagesDescriptionDevice">هل أنت متيقِّن من أنك تريد مسح هذه الرسائل من هذا الجهاز فقط؟</string>
<string name="deleteMessagesDescriptionEveryone">هل أنت متيقِّن من أنك تريد مسح هذه الرسائل لدى الجميع؟</string> <string name="deleteMessagesDescriptionEveryone">هل أنت متيقِّن من أنك تريد مسح هذه الرسائل لدى الجميع؟</string>
<string name="deleting">حذف</string> <string name="deleting">حذف</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Mesajı sil</item> <item quantity="one">Mesajı sil</item>
<item quantity="other">Mesajları sil</item> <item quantity="other">Mesajları sil</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Bu mesajı silmək istədiyinizə əminsiniz?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mesaj silindi</item> <item quantity="one">Mesaj silindi</item>
<item quantity="other">Mesajlar silindi</item> <item quantity="other">Mesajlar silindi</item>
@ -298,7 +297,6 @@
<item quantity="one">Mesajın silinməsi uğursuz oldu</item> <item quantity="one">Mesajın silinməsi uğursuz oldu</item>
<item quantity="other">Mesajların silinməsi uğursuz oldu</item> <item quantity="other">Mesajların silinməsi uğursuz oldu</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Bu mesajları silmək istədiyinizə əminsiniz?</string>
<string name="deleteMessagesDescriptionDevice">Bu mesajları yalnız bu cihazdan silmək istədiyinizə əminsiniz?</string> <string name="deleteMessagesDescriptionDevice">Bu mesajları yalnız bu cihazdan silmək istədiyinizə əminsiniz?</string>
<string name="deleteMessagesDescriptionEveryone">Bu mesajları hər kəs üçün silmək istədiyinizə əminsiniz?</string> <string name="deleteMessagesDescriptionEveryone">Bu mesajları hər kəs üçün silmək istədiyinizə əminsiniz?</string>
<string name="deleting">Silinir</string> <string name="deleting">Silinir</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Delete Message</item> <item quantity="one">Delete Message</item>
<item quantity="other">Delete Messages</item> <item quantity="other">Delete Messages</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">دم کی لحاظ انت کہ ایی میسج ھذب بکنی؟</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Message deleted</item> <item quantity="one">Message deleted</item>
<item quantity="other">Messages deleted</item> <item quantity="other">Messages deleted</item>
@ -297,7 +296,6 @@
<item quantity="one">پیگام مٹ بوت ناکام بِئن</item> <item quantity="one">پیگام مٹ بوت ناکام بِئن</item>
<item quantity="other">پیغامانی مٹ بوت ناکام بِئن</item> <item quantity="other">پیغامانی مٹ بوت ناکام بِئن</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">دم کی لحاظ انت که ایی امیسجات ھذب بکنی؟</string>
<string name="deleteMessagesDescriptionDevice">کیا آپ یقیناً یہ پیغامات صرف اس ڈیوائس سے حذف کرنا چاہتے ہیں؟</string> <string name="deleteMessagesDescriptionDevice">کیا آپ یقیناً یہ پیغامات صرف اس ڈیوائس سے حذف کرنا چاہتے ہیں؟</string>
<string name="deleteMessagesDescriptionEveryone">کیا آپ یقیناً یہ پیغامات سب کے لیے حذف کرنا چاہتے ہیں؟</string> <string name="deleteMessagesDescriptionEveryone">کیا آپ یقیناً یہ پیغامات سب کے لیے حذف کرنا چاہتے ہیں؟</string>
<string name="deleting">حذف کر رہا ہے</string> <string name="deleting">حذف کر رہا ہے</string>

View File

@ -285,7 +285,6 @@
<item quantity="many">Выдаліць паведамленні</item> <item quantity="many">Выдаліць паведамленні</item>
<item quantity="other">Выдаліць паведамленні</item> <item quantity="other">Выдаліць паведамленні</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Вы ўпэўненыя, што жадаеце выдаліць гэтае паведамленне?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Паведамленне выдалена</item> <item quantity="one">Паведамленне выдалена</item>
<item quantity="few">Паведамленні выдалены</item> <item quantity="few">Паведамленні выдалены</item>
@ -305,7 +304,6 @@
<item quantity="many">Не атрымалася выдаліць паведамленні</item> <item quantity="many">Не атрымалася выдаліць паведамленні</item>
<item quantity="other">Не атрымалася выдаліць паведамленні</item> <item quantity="other">Не атрымалася выдаліць паведамленні</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Вы ўпэўненыя, што жадаеце выдаліць гэтыя паведамленні?</string>
<string name="deleteMessagesDescriptionDevice">Вы ўпэўнены, што жадаеце выдаліць гэтыя паведамленні толькі з гэтай прылады?</string> <string name="deleteMessagesDescriptionDevice">Вы ўпэўнены, што жадаеце выдаліць гэтыя паведамленні толькі з гэтай прылады?</string>
<string name="deleteMessagesDescriptionEveryone">Вы ўпэўнены, што жадаеце выдаліць гэтыя паведамленні для ўсіх?</string> <string name="deleteMessagesDescriptionEveryone">Вы ўпэўнены, што жадаеце выдаліць гэтыя паведамленні для ўсіх?</string>
<string name="deleting">Выдаленне</string> <string name="deleting">Выдаленне</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Изтрий съобщението</item> <item quantity="one">Изтрий съобщението</item>
<item quantity="other">Изтрий съобщенията</item> <item quantity="other">Изтрий съобщенията</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Сигурен ли си, че желаеш да изтриеш това съобщение?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Съобщението е изтрито</item> <item quantity="one">Съобщението е изтрито</item>
<item quantity="other">Съобщенията са изтрити</item> <item quantity="other">Съобщенията са изтрити</item>
@ -297,7 +296,6 @@
<item quantity="one">Неуспешно изтриване на съобщение</item> <item quantity="one">Неуспешно изтриване на съобщение</item>
<item quantity="other">Неуспешно изтриване на съобщения</item> <item quantity="other">Неуспешно изтриване на съобщения</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Сигурен ли си, че искаш да изтриеш тези съобщения?</string>
<string name="deleteMessagesDescriptionDevice">Сигурен ли/ли сте, че искате да изтриете тези съобщения само от това устройство?</string> <string name="deleteMessagesDescriptionDevice">Сигурен ли/ли сте, че искате да изтриете тези съобщения само от това устройство?</string>
<string name="deleteMessagesDescriptionEveryone">Сигурен ли/ли сте, че искате да изтриете тези съобщения за всички?</string> <string name="deleteMessagesDescriptionEveryone">Сигурен ли/ли сте, че искате да изтриете тези съобщения за всички?</string>
<string name="deleting">Изтриване</string> <string name="deleting">Изтриване</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">বার্তা মুছুন</item> <item quantity="one">বার্তা মুছুন</item>
<item quantity="other">বার্তাগুলি মুছুন</item> <item quantity="other">বার্তাগুলি মুছুন</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">আপনি কি এই বার্তাটি মুছে দিতে নিশ্চিত?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">বার্তা মুছে ফেলা হয়েছে</item> <item quantity="one">বার্তা মুছে ফেলা হয়েছে</item>
<item quantity="other">বার্তাগুলি মুছে ফেলা হয়েছে</item> <item quantity="other">বার্তাগুলি মুছে ফেলা হয়েছে</item>
@ -297,7 +296,6 @@
<item quantity="one">Failed to delete message</item> <item quantity="one">Failed to delete message</item>
<item quantity="other">Failed to delete messages</item> <item quantity="other">Failed to delete messages</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">আপনি কি এই বার্তাগুলি মুছে ফেলতে চান?</string>
<string name="deleteMessagesDescriptionDevice">আপনি কি নিশ্চিত যে আপনি এই বার্তাগুলি শুধুমাত্র এই ডিভাইস থেকে মুছে ফেলতে চান?</string> <string name="deleteMessagesDescriptionDevice">আপনি কি নিশ্চিত যে আপনি এই বার্তাগুলি শুধুমাত্র এই ডিভাইস থেকে মুছে ফেলতে চান?</string>
<string name="deleteMessagesDescriptionEveryone">আপনি কি নিশ্চিত যে আপনি এইবার্তাগুলো সবাইকে জন্য মুছে ফেলতে চান?</string> <string name="deleteMessagesDescriptionEveryone">আপনি কি নিশ্চিত যে আপনি এইবার্তাগুলো সবাইকে জন্য মুছে ফেলতে চান?</string>
<string name="deleting">মুছে ফেলা হচ্ছে</string> <string name="deleting">মুছে ফেলা হচ্ছে</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Suprimeix el missatge</item> <item quantity="one">Suprimeix el missatge</item>
<item quantity="other">Suprimeix els missatges</item> <item quantity="other">Suprimeix els missatges</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Esteu segur que voleu suprimir aquest missatge?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Missatge suprimit</item> <item quantity="one">Missatge suprimit</item>
<item quantity="other">Missatges suprimits</item> <item quantity="other">Missatges suprimits</item>
@ -297,7 +296,6 @@
<item quantity="one">Error en eliminar el missatge</item> <item quantity="one">Error en eliminar el missatge</item>
<item quantity="other">Error en eliminar els missatges</item> <item quantity="other">Error en eliminar els missatges</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Esteu segur que voleu suprimir aquests missatges?</string>
<string name="deleteMessagesDescriptionDevice">Esteu segur que voleu esborrar aquests missatges només d\'aquest dispositiu?</string> <string name="deleteMessagesDescriptionDevice">Esteu segur que voleu esborrar aquests missatges només d\'aquest dispositiu?</string>
<string name="deleteMessagesDescriptionEveryone">Esteu segur que voleu suprimir aquests missatges per a tothom?</string> <string name="deleteMessagesDescriptionEveryone">Esteu segur que voleu suprimir aquests missatges per a tothom?</string>
<string name="deleting">Suprimint</string> <string name="deleting">Suprimint</string>

View File

@ -286,7 +286,6 @@
<item quantity="many">Smazat zprávy</item> <item quantity="many">Smazat zprávy</item>
<item quantity="other">Smazat zprávy</item> <item quantity="other">Smazat zprávy</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Opravdu chcete smazat tuto zprávu?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Zpráva smazána</item> <item quantity="one">Zpráva smazána</item>
<item quantity="few">Zprávy smazány</item> <item quantity="few">Zprávy smazány</item>
@ -306,7 +305,6 @@
<item quantity="many">Nepodařilo se smazat zprávy</item> <item quantity="many">Nepodařilo se smazat zprávy</item>
<item quantity="other">Nepodařilo se smazat zprávy</item> <item quantity="other">Nepodařilo se smazat zprávy</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Opravdu chcete smazat tyto zprávy?</string>
<string name="deleteMessagesDescriptionDevice">Jste si jisti, že chcete smazat tyto zprávy pouze z tohoto zařízení?</string> <string name="deleteMessagesDescriptionDevice">Jste si jisti, že chcete smazat tyto zprávy pouze z tohoto zařízení?</string>
<string name="deleteMessagesDescriptionEveryone">Jste si jisti, že chcete smazat tyto zprávy pro všechny?</string> <string name="deleteMessagesDescriptionEveryone">Jste si jisti, že chcete smazat tyto zprávy pro všechny?</string>
<string name="deleting">Mazání</string> <string name="deleting">Mazání</string>

View File

@ -290,7 +290,6 @@
<item quantity="many">Dileu Negeseuon</item> <item quantity="many">Dileu Negeseuon</item>
<item quantity="other">Dileu Negeseuon</item> <item quantity="other">Dileu Negeseuon</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Ydych chi\'n siŵr eich bod am ddileu\'r neges hon?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="zero">Negeseuon wedi\'u dileu</item> <item quantity="zero">Negeseuon wedi\'u dileu</item>
<item quantity="one">Neges wedi\'i dileu</item> <item quantity="one">Neges wedi\'i dileu</item>
@ -314,7 +313,6 @@
<item quantity="many">Methwyd dileu negeseuon</item> <item quantity="many">Methwyd dileu negeseuon</item>
<item quantity="other">Methu dileu negeseuon</item> <item quantity="other">Methu dileu negeseuon</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Ydych chi\'n siŵr eich bod am ddileu\'r neges(au) hyn?</string>
<string name="deleteMessagesDescriptionDevice">Ydych chi\'n siŵr eich bod am ddileu\'r negeseuon hyn o\'r ddyfais hon yn unig?</string> <string name="deleteMessagesDescriptionDevice">Ydych chi\'n siŵr eich bod am ddileu\'r negeseuon hyn o\'r ddyfais hon yn unig?</string>
<string name="deleteMessagesDescriptionEveryone">Ydych chi\'n siŵr eich bod am ddileu\'r negeseuon hyn i bawb?</string> <string name="deleteMessagesDescriptionEveryone">Ydych chi\'n siŵr eich bod am ddileu\'r negeseuon hyn i bawb?</string>
<string name="deleting">Wrthi\'n dileu</string> <string name="deleting">Wrthi\'n dileu</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Slet besked</item> <item quantity="one">Slet besked</item>
<item quantity="other">Slet beskeder</item> <item quantity="other">Slet beskeder</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Er du sikker på, at du vil slette denne besked?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Besked slettet</item> <item quantity="one">Besked slettet</item>
<item quantity="other">Beskeder slettet</item> <item quantity="other">Beskeder slettet</item>
@ -297,7 +296,6 @@
<item quantity="one">Kunne ikke slette besked</item> <item quantity="one">Kunne ikke slette besked</item>
<item quantity="other">Kunne ikke slette beskeder</item> <item quantity="other">Kunne ikke slette beskeder</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Er du sikker på, at du vil slette disse beskeder?</string>
<string name="deleteMessagesDescriptionDevice">Er du sikker på, at du vil slette disse beskeder kun fra denne enhed?</string> <string name="deleteMessagesDescriptionDevice">Er du sikker på, at du vil slette disse beskeder kun fra denne enhed?</string>
<string name="deleteMessagesDescriptionEveryone">Er du sikker på, at du vil slette disse beskeder for alle?</string> <string name="deleteMessagesDescriptionEveryone">Er du sikker på, at du vil slette disse beskeder for alle?</string>
<string name="deleting">Sletter</string> <string name="deleting">Sletter</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Nachricht löschen</item> <item quantity="one">Nachricht löschen</item>
<item quantity="other">Nachrichten löschen</item> <item quantity="other">Nachrichten löschen</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Möchtest du diese Nachricht wirklich löschen?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Nachricht gelöscht</item> <item quantity="one">Nachricht gelöscht</item>
<item quantity="other">Nachrichten gelöscht</item> <item quantity="other">Nachrichten gelöscht</item>
@ -298,7 +297,6 @@
<item quantity="one">Die Nachricht konnte nicht gelöscht werden</item> <item quantity="one">Die Nachricht konnte nicht gelöscht werden</item>
<item quantity="other">Die Nachrichten konnten nicht gelöscht werden</item> <item quantity="other">Die Nachrichten konnten nicht gelöscht werden</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Möchtest du diese Nachrichten wirklich löschen?</string>
<string name="deleteMessagesDescriptionDevice">Möchtest du diese Nachrichten wirklich nur von diesem Gerät löschen?</string> <string name="deleteMessagesDescriptionDevice">Möchtest du diese Nachrichten wirklich nur von diesem Gerät löschen?</string>
<string name="deleteMessagesDescriptionEveryone">Möchtest du diese Nachrichten wirklich für alle löschen?</string> <string name="deleteMessagesDescriptionEveryone">Möchtest du diese Nachrichten wirklich für alle löschen?</string>
<string name="deleting">Wird gelöscht</string> <string name="deleting">Wird gelöscht</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Διαγραφή Μηνύματος</item> <item quantity="one">Διαγραφή Μηνύματος</item>
<item quantity="other">Διαγραφή Μηνυμάτων</item> <item quantity="other">Διαγραφή Μηνυμάτων</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Σίγουρα θέλετε να διαγράψετε αυτό το μήνυμα;</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Το μήνυμα διαγράφηκε</item> <item quantity="one">Το μήνυμα διαγράφηκε</item>
<item quantity="other">Τα μηνύματα διαγράφηκαν</item> <item quantity="other">Τα μηνύματα διαγράφηκαν</item>
@ -298,7 +297,6 @@
<item quantity="one">Αποτυχία διαγραφής μηνύματος</item> <item quantity="one">Αποτυχία διαγραφής μηνύματος</item>
<item quantity="other">Αποτυχία διαγραφής μηνύματος</item> <item quantity="other">Αποτυχία διαγραφής μηνύματος</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Σίγουρα θέλετε να διαγράψετε αυτά τα μηνύματα;</string>
<string name="deleteMessagesDescriptionDevice">Σίγουρα θέλετε να διαγράψετε αυτά τα μηνύματα μόνο από αυτή τη συσκευή;</string> <string name="deleteMessagesDescriptionDevice">Σίγουρα θέλετε να διαγράψετε αυτά τα μηνύματα μόνο από αυτή τη συσκευή;</string>
<string name="deleteMessagesDescriptionEveryone">Σίγουρα θέλετε να διαγράψετε αυτά τα μηνύματα για όλους;</string> <string name="deleteMessagesDescriptionEveryone">Σίγουρα θέλετε να διαγράψετε αυτά τα μηνύματα για όλους;</string>
<string name="deleting">Γίνεται διαγραφή</string> <string name="deleting">Γίνεται διαγραφή</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Forigi mesaĝon</item> <item quantity="one">Forigi mesaĝon</item>
<item quantity="other">Forigi mesaĝojn</item> <item quantity="other">Forigi mesaĝojn</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Ĉu vi certas, ke vi volas forigi ĉi tiun mesaĝon?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mesaĝo forigita</item> <item quantity="one">Mesaĝo forigita</item>
<item quantity="other">Mesaĝoj forigitaj</item> <item quantity="other">Mesaĝoj forigitaj</item>
@ -297,7 +296,6 @@
<item quantity="one">Malsukcesis forigi mesaĝon</item> <item quantity="one">Malsukcesis forigi mesaĝon</item>
<item quantity="other">Malsukcesis forigi mesaĝojn</item> <item quantity="other">Malsukcesis forigi mesaĝojn</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Ĉu vi certas forigi tiujn mesaĝojn?</string>
<string name="deleteMessagesDescriptionDevice">Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn nur el ĉi tiu aparato?</string> <string name="deleteMessagesDescriptionDevice">Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn nur el ĉi tiu aparato?</string>
<string name="deleteMessagesDescriptionEveryone">Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn por ĉiuj?</string> <string name="deleteMessagesDescriptionEveryone">Ĉu vi certas, ke vi volas forigi ĉi tiujn mesaĝojn por ĉiuj?</string>
<string name="deleting">Forviŝante</string> <string name="deleting">Forviŝante</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Eliminar el mensaje</item> <item quantity="one">Eliminar el mensaje</item>
<item quantity="other">Eliminar el mensaje</item> <item quantity="other">Eliminar el mensaje</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">¿Estás seguro de que deseas eliminar este mensaje?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mensaje eliminado</item> <item quantity="one">Mensaje eliminado</item>
<item quantity="other">Mensajes eliminados</item> <item quantity="other">Mensajes eliminados</item>
@ -298,7 +297,6 @@
<item quantity="one">Error al eliminar el mensaje</item> <item quantity="one">Error al eliminar el mensaje</item>
<item quantity="other">Error al eliminar los mensajes</item> <item quantity="other">Error al eliminar los mensajes</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">¿Estás seguro de que deseas eliminar estos mensajes?</string>
<string name="deleteMessagesDescriptionDevice">¿Estás seguro de que quieres eliminar estos mensajes solo de este dispositivo?</string> <string name="deleteMessagesDescriptionDevice">¿Estás seguro de que quieres eliminar estos mensajes solo de este dispositivo?</string>
<string name="deleteMessagesDescriptionEveryone">¿Estás seguro de que quieres eliminar estos mensajes para todos?</string> <string name="deleteMessagesDescriptionEveryone">¿Estás seguro de que quieres eliminar estos mensajes para todos?</string>
<string name="deleting">Eliminando</string> <string name="deleting">Eliminando</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Eliminar Mensaje</item> <item quantity="one">Eliminar Mensaje</item>
<item quantity="other">Eliminar Mensajes</item> <item quantity="other">Eliminar Mensajes</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">¿Estás seguro de que quieres eliminar este mensaje?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mensaje borrado</item> <item quantity="one">Mensaje borrado</item>
<item quantity="other">Mensajes borrados</item> <item quantity="other">Mensajes borrados</item>
@ -298,7 +297,6 @@
<item quantity="one">Fallo al eliminar el mensaje</item> <item quantity="one">Fallo al eliminar el mensaje</item>
<item quantity="other">Fallo al eliminar los mensajes</item> <item quantity="other">Fallo al eliminar los mensajes</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">¿Estás seguro de que quieres eliminar estos mensajes?</string>
<string name="deleteMessagesDescriptionDevice">¿Estás seguro de querer eliminar estos mensajes solamente de este dispositivo?</string> <string name="deleteMessagesDescriptionDevice">¿Estás seguro de querer eliminar estos mensajes solamente de este dispositivo?</string>
<string name="deleteMessagesDescriptionEveryone">¿Estás seguro de querer eliminar estos mensajes para todos?</string> <string name="deleteMessagesDescriptionEveryone">¿Estás seguro de querer eliminar estos mensajes para todos?</string>
<string name="deleting">Eliminando</string> <string name="deleting">Eliminando</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Kustuta sõnum</item> <item quantity="one">Kustuta sõnum</item>
<item quantity="other">Kustuta sõnumid</item> <item quantity="other">Kustuta sõnumid</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Kas soovite selle sõnumi kustutada?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Sõnum kustutatud</item> <item quantity="one">Sõnum kustutatud</item>
<item quantity="other">Sõnumid kustutatud</item> <item quantity="other">Sõnumid kustutatud</item>
@ -297,7 +296,6 @@
<item quantity="one">Sõnumi kustutamine ebaõnnestus</item> <item quantity="one">Sõnumi kustutamine ebaõnnestus</item>
<item quantity="other">Sõnumite kustutamine ebaõnnestus</item> <item quantity="other">Sõnumite kustutamine ebaõnnestus</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Kas soovite need sõnumid kustutada?</string>
<string name="deleteMessagesDescriptionDevice">Kas olete kindel, et soovite need sõnumid kustutada ainult sellest seadmest?</string> <string name="deleteMessagesDescriptionDevice">Kas olete kindel, et soovite need sõnumid kustutada ainult sellest seadmest?</string>
<string name="deleteMessagesDescriptionEveryone">Kas olete kindel, et soovite need sõnumid kõigi jaoks kustutada?</string> <string name="deleteMessagesDescriptionEveryone">Kas olete kindel, et soovite need sõnumid kõigi jaoks kustutada?</string>
<string name="deleting">Kustutan</string> <string name="deleting">Kustutan</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Mezua Ezabatu</item> <item quantity="one">Mezua Ezabatu</item>
<item quantity="other">Mezuak Ezabatu</item> <item quantity="other">Mezuak Ezabatu</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Ziur zaude mezu hau ezabatu nahi duzula?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mezua ezabatuta</item> <item quantity="one">Mezua ezabatuta</item>
<item quantity="other">Mezuak ezabatuta</item> <item quantity="other">Mezuak ezabatuta</item>
@ -297,7 +296,6 @@
<item quantity="one">Ezin izan da mezua ezabatu</item> <item quantity="one">Ezin izan da mezua ezabatu</item>
<item quantity="other">Ezin izan dira mezuak ezabatu</item> <item quantity="other">Ezin izan dira mezuak ezabatu</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Ziur zaude mezu hauek ezabatu nahi dituzula?</string>
<string name="deleteMessagesDescriptionDevice">Ziur zaude mezu hauek gailu honetatik soilik ezabatu nahi dituzula?</string> <string name="deleteMessagesDescriptionDevice">Ziur zaude mezu hauek gailu honetatik soilik ezabatu nahi dituzula?</string>
<string name="deleteMessagesDescriptionEveryone">Ziur zaude mezu hauek denentzat ezabatu nahi dituzula?</string> <string name="deleteMessagesDescriptionEveryone">Ziur zaude mezu hauek denentzat ezabatu nahi dituzula?</string>
<string name="deleting">Ezabatzen</string> <string name="deleting">Ezabatzen</string>

View File

@ -279,7 +279,6 @@
<item quantity="one">پیام را پاک کنید</item> <item quantity="one">پیام را پاک کنید</item>
<item quantity="other">پیام ها را پاک کنید</item> <item quantity="other">پیام ها را پاک کنید</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">آیا مطمئن هستید که می‌خواهید این پیام را حذف کنید؟</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">پیام پاک شد</item> <item quantity="one">پیام پاک شد</item>
<item quantity="other">پبام ها حذف شدند</item> <item quantity="other">پبام ها حذف شدند</item>
@ -295,7 +294,6 @@
<item quantity="one">خطا در حذف پیام</item> <item quantity="one">خطا در حذف پیام</item>
<item quantity="other">خطا در حذف پیام ها</item> <item quantity="other">خطا در حذف پیام ها</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">آیا مطمئن هستید که می‌خواهید این پیام‌ها را حذف کنید؟</string>
<string name="deleteMessagesDescriptionDevice">آیا مطمئن هستید می‌خواهید این پیام‌ها را فقط از این دستگاه حذف کنید؟</string> <string name="deleteMessagesDescriptionDevice">آیا مطمئن هستید می‌خواهید این پیام‌ها را فقط از این دستگاه حذف کنید؟</string>
<string name="deleteMessagesDescriptionEveryone">آیا مطمئن هستید می‌خواهید این پیام‌ها را برای همه حذف کنید؟</string> <string name="deleteMessagesDescriptionEveryone">آیا مطمئن هستید می‌خواهید این پیام‌ها را برای همه حذف کنید؟</string>
<string name="deleting">در حال حذف</string> <string name="deleting">در حال حذف</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Poista viesti</item> <item quantity="one">Poista viesti</item>
<item quantity="other">Poista viestit</item> <item quantity="other">Poista viestit</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Haluatko varmasti poistaa tämän viestin?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Viesti poistettu</item> <item quantity="one">Viesti poistettu</item>
<item quantity="other">Viestit poistettu</item> <item quantity="other">Viestit poistettu</item>
@ -298,7 +297,6 @@
<item quantity="one">Viestin poisto epäonnistui</item> <item quantity="one">Viestin poisto epäonnistui</item>
<item quantity="other">Viestien poisto epäonnistui</item> <item quantity="other">Viestien poisto epäonnistui</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Haluatko varmasti poistaa nämä viestit?</string>
<string name="deleteMessagesDescriptionDevice">Haluatko varmasti poistaa nämä viestit vain tästä laitteesta?</string> <string name="deleteMessagesDescriptionDevice">Haluatko varmasti poistaa nämä viestit vain tästä laitteesta?</string>
<string name="deleteMessagesDescriptionEveryone">Haluatko varmasti poistaa nämä viestit kaikilta?</string> <string name="deleteMessagesDescriptionEveryone">Haluatko varmasti poistaa nämä viestit kaikilta?</string>
<string name="deleting">Poistetaan</string> <string name="deleting">Poistetaan</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Burahin ang Mensahe</item> <item quantity="one">Burahin ang Mensahe</item>
<item quantity="other">Burahin ang mga Mensahe</item> <item quantity="other">Burahin ang mga Mensahe</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Sigurado ka bang gusto mong burahin ang mensaheng ito?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Mensahe nabura</item> <item quantity="one">Mensahe nabura</item>
<item quantity="other">Mga mensahe nabura</item> <item quantity="other">Mga mensahe nabura</item>
@ -297,7 +296,6 @@
<item quantity="one">Nabigong tanggalin ang mensahe</item> <item quantity="one">Nabigong tanggalin ang mensahe</item>
<item quantity="other">Nabigong tanggalin ang mga mensahe</item> <item quantity="other">Nabigong tanggalin ang mga mensahe</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Sigurado ka bang gusto mong burahin ang mga mensaheng ito?</string>
<string name="deleteMessagesDescriptionDevice">Sigurado ka bang gusto mong tanggalin ang mga mensaheng ito mula sa device na ito lamang?</string> <string name="deleteMessagesDescriptionDevice">Sigurado ka bang gusto mong tanggalin ang mga mensaheng ito mula sa device na ito lamang?</string>
<string name="deleteMessagesDescriptionEveryone">Sigurado ka bang gusto mong tanggalin ang mga mensaheng ito para sa lahat?</string> <string name="deleteMessagesDescriptionEveryone">Sigurado ka bang gusto mong tanggalin ang mga mensaheng ito para sa lahat?</string>
<string name="deleting">Binubura</string> <string name="deleting">Binubura</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Supprimer le message</item> <item quantity="one">Supprimer le message</item>
<item quantity="other">Supprimer les messages</item> <item quantity="other">Supprimer les messages</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Êtes-vous sûr de vouloir supprimer ce message ?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Message supprimé</item> <item quantity="one">Message supprimé</item>
<item quantity="other">Messages supprimés</item> <item quantity="other">Messages supprimés</item>
@ -298,7 +297,6 @@
<item quantity="one">Échec de suppression du message</item> <item quantity="one">Échec de suppression du message</item>
<item quantity="other">Échec de suppression des messages</item> <item quantity="other">Échec de suppression des messages</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Êtes-vous sûr de vouloir supprimer ces messages ?</string>
<string name="deleteMessagesDescriptionDevice">Êtes-vous certain·e de vouloir supprimer ces messages seulement sur cet appareil?</string> <string name="deleteMessagesDescriptionDevice">Êtes-vous certain·e de vouloir supprimer ces messages seulement sur cet appareil?</string>
<string name="deleteMessagesDescriptionEveryone">Êtes-vous certain·e de vouloir supprimer ces messages pour tout le monde?</string> <string name="deleteMessagesDescriptionEveryone">Êtes-vous certain·e de vouloir supprimer ces messages pour tout le monde?</string>
<string name="deleting">Suppression</string> <string name="deleting">Suppression</string>

View File

@ -260,7 +260,6 @@
<string name="deleteAfterLegacyDisappearingMessagesLegacy">Ancestral</string> <string name="deleteAfterLegacyDisappearingMessagesLegacy">Ancestral</string>
<string name="deleteAfterLegacyDisappearingMessagesOriginal">Versión orixinal das mensaxes que desaparecen.</string> <string name="deleteAfterLegacyDisappearingMessagesOriginal">Versión orixinal das mensaxes que desaparecen.</string>
<string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b> estableceu o temporizador de desaparición das mensaxes a <b>{time}</b></string> <string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b> estableceu o temporizador de desaparición das mensaxes a <b>{time}</b></string>
<string name="deleteMessageConfirm">Tes a certeza de querer borrar esta mensaxe?</string>
<string name="deleteMessageDeletedGlobally">Esta mensaxe foi eliminada</string> <string name="deleteMessageDeletedGlobally">Esta mensaxe foi eliminada</string>
<string name="deleteMessageDeletedLocally">Esta mensaxe foi eliminada neste dispositivo</string> <string name="deleteMessageDeletedLocally">Esta mensaxe foi eliminada neste dispositivo</string>
<string name="deleteMessageDescriptionDevice">Tes a certeza de querer borrar esta mensaxe só deste dispositivo?</string> <string name="deleteMessageDescriptionDevice">Tes a certeza de querer borrar esta mensaxe só deste dispositivo?</string>
@ -272,7 +271,6 @@
<item quantity="one">Erro ao borrar a mensaxe</item> <item quantity="one">Erro ao borrar a mensaxe</item>
<item quantity="other">Erro ao borrar as mensaxes</item> <item quantity="other">Erro ao borrar as mensaxes</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Tes a certeza de querer borrar estas mensaxes?</string>
<string name="deleteMessagesDescriptionDevice">Tes a certeza de querer eliminar estas mensaxes só deste dispositivo?</string> <string name="deleteMessagesDescriptionDevice">Tes a certeza de querer eliminar estas mensaxes só deste dispositivo?</string>
<string name="deleteMessagesDescriptionEveryone">Tes a certeza de querer eliminar estas mensaxes para todos?</string> <string name="deleteMessagesDescriptionEveryone">Tes a certeza de querer eliminar estas mensaxes para todos?</string>
<string name="deleting">Borrando</string> <string name="deleting">Borrando</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Goge Saƙo</item> <item quantity="one">Goge Saƙo</item>
<item quantity="other">Goge Saƙonni</item> <item quantity="other">Goge Saƙonni</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Ka tabbata kana so ka goge wannan saƙon?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">An goge saƙo</item> <item quantity="one">An goge saƙo</item>
<item quantity="other">An goge Saƙonni</item> <item quantity="other">An goge Saƙonni</item>
@ -297,7 +296,6 @@
<item quantity="one">An kasa share saƙo</item> <item quantity="one">An kasa share saƙo</item>
<item quantity="other">An kasa share saƙonni</item> <item quantity="other">An kasa share saƙonni</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Ka tabbata kana so ka goge waɗannan saƙonnin?</string>
<string name="deleteMessagesDescriptionDevice">Kana tabbata kana so ka share waɗannan saƙonnin daga wannan na\'urar kawai?</string> <string name="deleteMessagesDescriptionDevice">Kana tabbata kana so ka share waɗannan saƙonnin daga wannan na\'urar kawai?</string>
<string name="deleteMessagesDescriptionEveryone">Kana tabbata kana so ka share waɗannan saƙonnin don kowa?</string> <string name="deleteMessagesDescriptionEveryone">Kana tabbata kana so ka share waɗannan saƙonnin don kowa?</string>
<string name="deleting">Ana gogewa</string> <string name="deleting">Ana gogewa</string>

View File

@ -285,7 +285,6 @@
<item quantity="many">מחק הודעות</item> <item quantity="many">מחק הודעות</item>
<item quantity="other">מחק הודעות</item> <item quantity="other">מחק הודעות</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">האם אתה בטוח שברצונך למחוק את ההודעה הזו?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">הודעה נמחקה</item> <item quantity="one">הודעה נמחקה</item>
<item quantity="two">הודעות נמחקו</item> <item quantity="two">הודעות נמחקו</item>
@ -305,7 +304,6 @@
<item quantity="many">נכשל במחיקת הודעות</item> <item quantity="many">נכשל במחיקת הודעות</item>
<item quantity="other">נכשל במחיקת הודעות</item> <item quantity="other">נכשל במחיקת הודעות</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">אתה בטוח שברצונך למחוק הודעות אלה?</string>
<string name="deleteMessagesDescriptionDevice">האם אתה בטוח שברצונך למחוק את ההודעות האלו מהמכשיר הזה בלבד?</string> <string name="deleteMessagesDescriptionDevice">האם אתה בטוח שברצונך למחוק את ההודעות האלו מהמכשיר הזה בלבד?</string>
<string name="deleteMessagesDescriptionEveryone">האם אתה בטוח שברצונך למחוק את ההודעות האלה לכולם?</string> <string name="deleteMessagesDescriptionEveryone">האם אתה בטוח שברצונך למחוק את ההודעות האלה לכולם?</string>
<string name="deleting">מוחק</string> <string name="deleting">מוחק</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">संदेश मिटाएं</item> <item quantity="one">संदेश मिटाएं</item>
<item quantity="other">संदेश मिटाएं</item> <item quantity="other">संदेश मिटाएं</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">क्या आप वाकई इस संदेश को हटाना चाहते हैं?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">संदेश मिटाया गया</item> <item quantity="one">संदेश मिटाया गया</item>
<item quantity="other">संदेश मिटाये गए</item> <item quantity="other">संदेश मिटाये गए</item>
@ -298,7 +297,6 @@
<item quantity="one">संदेश हटाने में विफल</item> <item quantity="one">संदेश हटाने में विफल</item>
<item quantity="other">संदेशों को हटाने में विफल रहा</item> <item quantity="other">संदेशों को हटाने में विफल रहा</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">क्या आप वाकई इन संदेशों को मिटाना चाहते हैं?</string>
<string name="deleteMessagesDescriptionDevice">क्या आप वाकई केवल इस डिवाइस से इन संदेशों को हटाना चाहते हैं?</string> <string name="deleteMessagesDescriptionDevice">क्या आप वाकई केवल इस डिवाइस से इन संदेशों को हटाना चाहते हैं?</string>
<string name="deleteMessagesDescriptionEveryone">क्या आप वाकई सभी के लिए इन संदेशों को हटाना चाहते हैं?</string> <string name="deleteMessagesDescriptionEveryone">क्या आप वाकई सभी के लिए इन संदेशों को हटाना चाहते हैं?</string>
<string name="deleting">हटाया जा रहा है</string> <string name="deleting">हटाया जा रहा है</string>

View File

@ -283,7 +283,6 @@
<item quantity="few">Izbriši poruku</item> <item quantity="few">Izbriši poruku</item>
<item quantity="other">Izbriši poruku</item> <item quantity="other">Izbriši poruku</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Jeste li sigurni da želite izbrisati ovu poruku?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Poruka izbrisana</item> <item quantity="one">Poruka izbrisana</item>
<item quantity="few">Poruke obrisane</item> <item quantity="few">Poruke obrisane</item>
@ -301,7 +300,6 @@
<item quantity="few">Neuspješno brisanje poruka</item> <item quantity="few">Neuspješno brisanje poruka</item>
<item quantity="other">Neuspješno brisanje poruka</item> <item quantity="other">Neuspješno brisanje poruka</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Jeste li sigurni da želite izbrisati ove poruke?</string>
<string name="deleteMessagesDescriptionDevice">Jeste li sigurni da želite izbrisati ove poruke samo s ovog uređaja?</string> <string name="deleteMessagesDescriptionDevice">Jeste li sigurni da želite izbrisati ove poruke samo s ovog uređaja?</string>
<string name="deleteMessagesDescriptionEveryone">Jeste li sigurni da želite izbrisati ove poruke za sve?</string> <string name="deleteMessagesDescriptionEveryone">Jeste li sigurni da želite izbrisati ove poruke za sve?</string>
<string name="deleting">Brisanje</string> <string name="deleting">Brisanje</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Üzenet törlése</item> <item quantity="one">Üzenet törlése</item>
<item quantity="other">Üzenetek törlése</item> <item quantity="other">Üzenetek törlése</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Biztos, hogy törölni szeretnéd ezt az üzenetet?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Üzenet törölve</item> <item quantity="one">Üzenet törölve</item>
<item quantity="other">Üzenetek törölve</item> <item quantity="other">Üzenetek törölve</item>
@ -298,7 +297,6 @@
<item quantity="one">Nem sikerült az üzenet törlése</item> <item quantity="one">Nem sikerült az üzenet törlése</item>
<item quantity="other">Nem sikerült az üzenetek törlése</item> <item quantity="other">Nem sikerült az üzenetek törlése</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Biztos, hogy törölni szeretnéd ezeket az üzeneteket?</string>
<string name="deleteMessagesDescriptionDevice">Biztos, hogy törölni szeretnéd ezeket az üzeneteket csak ebből az eszközről?</string> <string name="deleteMessagesDescriptionDevice">Biztos, hogy törölni szeretnéd ezeket az üzeneteket csak ebből az eszközről?</string>
<string name="deleteMessagesDescriptionEveryone">Biztos, hogy törölni szeretnéd ezeket az üzeneteket mindenki számára?</string> <string name="deleteMessagesDescriptionEveryone">Biztos, hogy törölni szeretnéd ezeket az üzeneteket mindenki számára?</string>
<string name="deleting">Törlés</string> <string name="deleting">Törlés</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Ջնջել հաղորդագրությունը</item> <item quantity="one">Ջնջել հաղորդագրությունը</item>
<item quantity="other">Ջնջել հաղորդագրությունները</item> <item quantity="other">Ջնջել հաղորդագրությունները</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Վստա՞հ եք, որ ուզում եք ջնջել այս հաղորդագրությունը:</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Հաղորդագրությունը ջնջված է</item> <item quantity="one">Հաղորդագրությունը ջնջված է</item>
<item quantity="other">Հաղորդագրությունները ջնջված են</item> <item quantity="other">Հաղորդագրությունները ջնջված են</item>
@ -297,7 +296,6 @@
<item quantity="one">Չհաջողվեց ջնջել հաղորդագրությունը</item> <item quantity="one">Չհաջողվեց ջնջել հաղորդագրությունը</item>
<item quantity="other">Չհաջողվեց ջնջել հաղորդագրությունները</item> <item quantity="other">Չհաջողվեց ջնջել հաղորդագրությունները</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Վստա՞հ եք, որ ուզում եք ջնջել այս հաղորդագրություններն:</string>
<string name="deleteMessagesDescriptionDevice">Իսկապե՞ս ուզում եք ջնջել այս հաղորդագրությունները միայն այս սարքից:</string> <string name="deleteMessagesDescriptionDevice">Իսկապե՞ս ուզում եք ջնջել այս հաղորդագրությունները միայն այս սարքից:</string>
<string name="deleteMessagesDescriptionEveryone">Իսկապե՞ս ուզում եք ջնջել այս հաղորդագրությունները բոլորի համար:</string> <string name="deleteMessagesDescriptionEveryone">Իսկապե՞ս ուզում եք ջնջել այս հաղորդագրությունները բոլորի համար:</string>
<string name="deleting">Ջնջվում է</string> <string name="deleting">Ջնջվում է</string>

View File

@ -279,7 +279,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">Hapus Pesan</item> <item quantity="other">Hapus Pesan</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Apakah Anda yakin ingin menghapus pesan ini?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">Pesan dihapus</item> <item quantity="other">Pesan dihapus</item>
</plurals> </plurals>
@ -293,7 +292,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">Gagal menghapus pesan</item> <item quantity="other">Gagal menghapus pesan</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Apakah Anda yakin ingin menghapus pesan-pesan ini?</string>
<string name="deleteMessagesDescriptionDevice">Anda yakin ingin menghapus pesan ini hanya dari perangkat ini?</string> <string name="deleteMessagesDescriptionDevice">Anda yakin ingin menghapus pesan ini hanya dari perangkat ini?</string>
<string name="deleteMessagesDescriptionEveryone">Anda yakin ingin menghapus pesan-pesan ini untuk semua orang?</string> <string name="deleteMessagesDescriptionEveryone">Anda yakin ingin menghapus pesan-pesan ini untuk semua orang?</string>
<string name="deleting">Menghapus</string> <string name="deleting">Menghapus</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Elimina Messaggio</item> <item quantity="one">Elimina Messaggio</item>
<item quantity="other">Elimina Messaggi</item> <item quantity="other">Elimina Messaggi</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Sei sicuro di voler eliminare questo messaggio?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Messaggio eliminato</item> <item quantity="one">Messaggio eliminato</item>
<item quantity="other">Messaggi eliminati</item> <item quantity="other">Messaggi eliminati</item>
@ -298,7 +297,6 @@
<item quantity="one">Impossibile eliminare il messaggio</item> <item quantity="one">Impossibile eliminare il messaggio</item>
<item quantity="other">Impossibile eliminare i messaggi</item> <item quantity="other">Impossibile eliminare i messaggi</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Sei sicuro di voler eliminare questi messaggi?</string>
<string name="deleteMessagesDescriptionDevice">Sei sicuro di voler eliminare questi messaggi solo da questo dispositivo?</string> <string name="deleteMessagesDescriptionDevice">Sei sicuro di voler eliminare questi messaggi solo da questo dispositivo?</string>
<string name="deleteMessagesDescriptionEveryone">Sei sicuro di voler eliminare questi messaggi per tutti?</string> <string name="deleteMessagesDescriptionEveryone">Sei sicuro di voler eliminare questi messaggi per tutti?</string>
<string name="deleting">Eliminazione</string> <string name="deleting">Eliminazione</string>

View File

@ -280,7 +280,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">メッセージを削除</item> <item quantity="other">メッセージを削除</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">本当にこのメッセージを削除しますか?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">メッセージが削除されました</item> <item quantity="other">メッセージが削除されました</item>
</plurals> </plurals>
@ -294,7 +293,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">メッセージの削除に失敗しました</item> <item quantity="other">メッセージの削除に失敗しました</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">本当にこれらのメッセージを削除しますか?</string>
<string name="deleteMessagesDescriptionDevice">このデバイスからのみメッセージを削除してもよろしいですか?</string> <string name="deleteMessagesDescriptionDevice">このデバイスからのみメッセージを削除してもよろしいですか?</string>
<string name="deleteMessagesDescriptionEveryone">すべてのユーザーのメッセージを削除してもよろしいですか?</string> <string name="deleteMessagesDescriptionEveryone">すべてのユーザーのメッセージを削除してもよろしいですか?</string>
<string name="deleting">削除中</string> <string name="deleting">削除中</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">შეტყობინების წაშლა</item> <item quantity="one">შეტყობინების წაშლა</item>
<item quantity="other">შეტყობინებების წაშლა</item> <item quantity="other">შეტყობინებების წაშლა</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინების წაშლა?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">შეტყობინება წაშლილია</item> <item quantity="one">შეტყობინება წაშლილია</item>
<item quantity="other">შეტყობინებები წაშლილია</item> <item quantity="other">შეტყობინებები წაშლილია</item>
@ -297,7 +296,6 @@
<item quantity="one">შეტყობინების წაშლა ვერ მოხერხდა</item> <item quantity="one">შეტყობინების წაშლა ვერ მოხერხდა</item>
<item quantity="other">შეტყობინებების წაშლა ვერ მოხერხდა</item> <item quantity="other">შეტყობინებების წაშლა ვერ მოხერხდა</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინებების წაშლა?</string>
<string name="deleteMessagesDescriptionDevice">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინებების წაშლა მხოლოდ ამ მოწყობილობიდან?</string> <string name="deleteMessagesDescriptionDevice">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინებების წაშლა მხოლოდ ამ მოწყობილობიდან?</string>
<string name="deleteMessagesDescriptionEveryone">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინებების წაშლა ყველასთვის?</string> <string name="deleteMessagesDescriptionEveryone">დარწმუნებული ხართ, რომ გსურთ ამ შეტყობინებების წაშლა ყველასთვის?</string>
<string name="deleting">წაშლა მიმდინარეობს</string> <string name="deleting">წაშლა მიმდინარეობს</string>

View File

@ -279,7 +279,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">លុបសារ</item> <item quantity="other">លុបសារ</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">តើអ្នកប្រាកដទេថាចង់លុបសារនេះ?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">សារត្រូវបានលុបហើយ</item> <item quantity="other">សារត្រូវបានលុបហើយ</item>
</plurals> </plurals>
@ -293,7 +292,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">បរាជ័យក្នុងការលុបសារ</item> <item quantity="other">បរាជ័យក្នុងការលុបសារ</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">តើអ្នកប្រាកដទេថាចង់លុបសារទាំងនេះ?</string>
<string name="deleteMessagesDescriptionDevice">តើអ្នកប្រាកដទេថាអ្នកចង់លុបសារទាំងនេះពីឧបករណ៍នេះតែប៉ុណ្ណោះ?</string> <string name="deleteMessagesDescriptionDevice">តើអ្នកប្រាកដទេថាអ្នកចង់លុបសារទាំងនេះពីឧបករណ៍នេះតែប៉ុណ្ណោះ?</string>
<string name="deleteMessagesDescriptionEveryone">តើអ្នកប្រាកដទេថាអ្នកចង់លុបសារទាំងនេះសម្រាប់ចោល?</string> <string name="deleteMessagesDescriptionEveryone">តើអ្នកប្រាកដទេថាអ្នកចង់លុបសារទាំងនេះសម្រាប់ចោល?</string>
<string name="deleting">កំពុងលុប</string> <string name="deleting">កំពុងលុប</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Peyamê Jê Bibe</item> <item quantity="one">Peyamê Jê Bibe</item>
<item quantity="other">Peyaman Jê Bibe</item> <item quantity="other">Peyaman Jê Bibe</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Tu piştrast î ku tu dixwazî vê peyamê jê bibî?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Peyam hate rakirin</item> <item quantity="one">Peyam hate rakirin</item>
<item quantity="other">Peyamên hate rakirin</item> <item quantity="other">Peyamên hate rakirin</item>
@ -298,7 +297,6 @@
<item quantity="one">Bi ser neket ku peyama jê derbike.</item> <item quantity="one">Bi ser neket ku peyama jê derbike.</item>
<item quantity="other">Bi ser neket ku peyaman jê bibe.</item> <item quantity="other">Bi ser neket ku peyaman jê bibe.</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Tu piştrast î ku tu dixwazî peyamên van jê bibî?</string>
<string name="deleteMessagesDescriptionDevice">Tu piştrast î ku tu dixwazî vê peyaman tenê li ser cîhaza vê peyaman jê bibî?</string> <string name="deleteMessagesDescriptionDevice">Tu piştrast î ku tu dixwazî vê peyaman tenê li ser cîhaza vê peyaman jê bibî?</string>
<string name="deleteMessagesDescriptionEveryone">Tu piştrast î ku tu dixwazî vê peyaman ji kuran jê bike?</string> <string name="deleteMessagesDescriptionEveryone">Tu piştrast î ku tu dixwazî vê peyaman ji kuran jê bike?</string>
<string name="deleting">Jêbibin</string> <string name="deleting">Jêbibin</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಿ</item> <item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಿ</item>
<item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ</item> <item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಿ</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">ನೀವು ಈ ಸಂದೇಶವನ್ನು ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಲಾಗಿದೆ</item> <item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಲಾಗಿದೆ</item>
<item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ</item> <item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲಾಗಿದೆ</item>
@ -297,7 +296,6 @@
<item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಲು ವಿಫಲವಾಯಿತು</item> <item quantity="one">ಸಂದೇಶವನ್ನು ಅಳಿಸಲು ವಿಫಲವಾಯಿತು</item>
<item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲು ವಿಫಲವಾಯಿತು</item> <item quantity="other">ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲು ವಿಫಲವಾಯಿತು</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">ನೀವು ಈ ಸಂದೇಶಗಳನ್ನು ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string>
<string name="deleteMessagesDescriptionDevice">ನೀವು ಈ ಸಂದೇಶಗಳನ್ನು ಈ ಸಾಧನದಿಂದ ಮಾತ್ರ ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string> <string name="deleteMessagesDescriptionDevice">ನೀವು ಈ ಸಂದೇಶಗಳನ್ನು ಈ ಸಾಧನದಿಂದ ಮಾತ್ರ ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string>
<string name="deleteMessagesDescriptionEveryone">ನೀವು ಈ ಸಂದೇಶಗಳನ್ನು ಎಲ್ಲರಿಗಾಗಿ ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string> <string name="deleteMessagesDescriptionEveryone">ನೀವು ಈ ಸಂದೇಶಗಳನ್ನು ಎಲ್ಲರಿಗಾಗಿ ಅಳಿಸಲು ಖಚಿತವಾಗಿದ್ದೀರಾ?</string>
<string name="deleting">ಅಳಿಸಲಾಗುತ್ತಿದೆ</string> <string name="deleting">ಅಳಿಸಲಾಗುತ್ತಿದೆ</string>

View File

@ -280,7 +280,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">메시지 삭제</item> <item quantity="other">메시지 삭제</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">정말 이 메시지를 삭제하시겠습니까?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">메시지가 삭제되었습니다.</item> <item quantity="other">메시지가 삭제되었습니다.</item>
</plurals> </plurals>
@ -294,7 +293,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">메시지를 삭제하지 못했습니다</item> <item quantity="other">메시지를 삭제하지 못했습니다</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">정말 이 메시지를 삭제하시겠습니까?</string>
<string name="deleteMessagesDescriptionDevice">Are you sure you want to delete these messages from this device only?</string> <string name="deleteMessagesDescriptionDevice">Are you sure you want to delete these messages from this device only?</string>
<string name="deleteMessagesDescriptionEveryone">Are you sure you want to delete these messages for everyone?</string> <string name="deleteMessagesDescriptionEveryone">Are you sure you want to delete these messages for everyone?</string>
<string name="deleting">삭제 중…</string> <string name="deleting">삭제 중…</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">سڕینەوەی پەیام</item> <item quantity="one">سڕینەوەی پەیام</item>
<item quantity="other">سڕینەوەی پەیامەکان</item> <item quantity="other">سڕینەوەی پەیامەکان</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">دڵنیایت دەتەوێت ئەم پەیامە بسڕیتەوە؟</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">نامە سڕا</item> <item quantity="one">نامە سڕا</item>
<item quantity="other">نامەکان بسڕانەوە</item> <item quantity="other">نامەکان بسڕانەوە</item>
@ -297,7 +296,6 @@
<item quantity="one">شکستی هەندەڵکردنی پەیام</item> <item quantity="one">شکستی هەندەڵکردنی پەیام</item>
<item quantity="other">شکستی هەندەڵکردنی پەیامەکان</item> <item quantity="other">شکستی هەندەڵکردنی پەیامەکان</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">دڵنیایت دەتەوێت ئەم پەیامانە بسڕیتەوە؟</string>
<string name="deleteMessagesDescriptionDevice">دڵنیایت بۆ سڕینەوەی هەموو پەیام دەتێنا زاتیە؟</string> <string name="deleteMessagesDescriptionDevice">دڵنیایت بۆ سڕینەوەی هەموو پەیام دەتێنا زاتیە؟</string>
<string name="deleteMessagesDescriptionEveryone">دڵنیایت بۆ سڕینەوەی ئەم پەیامەکان بۆ هەموو ؟</string> <string name="deleteMessagesDescriptionEveryone">دڵنیایت بۆ سڕینەوەی ئەم پەیامەکان بۆ هەموو ؟</string>
<string name="deleting">سڕینەوە</string> <string name="deleting">سڕینەوە</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Jjamu Olukome ngaleerake</item> <item quantity="one">Jjamu Olukome ngaleerake</item>
<item quantity="other">Jjamu Ente</item> <item quantity="other">Jjamu Ente</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Oli mukakafu nti oyagala okusazaamu obubaka buno?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Obubaka bukyusiddwako</item> <item quantity="one">Obubaka bukyusiddwako</item>
<item quantity="other">Obubaka obwokutorera obukyusiddwako</item> <item quantity="other">Obubaka obwokutorera obukyusiddwako</item>
@ -297,7 +296,6 @@
<item quantity="one">Kusazaamu obubaka kugaanye</item> <item quantity="one">Kusazaamu obubaka kugaanye</item>
<item quantity="other">Kusazaamu obubaka kugaanye</item> <item quantity="other">Kusazaamu obubaka kugaanye</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Oli mukakafu nti oyagala okusazaamu obubaka buno?</string>
<string name="deleteMessagesDescriptionDevice">Oli mbanankubye kusula ebubaka bino ku kyuma kino kyokka?</string> <string name="deleteMessagesDescriptionDevice">Oli mbanankubye kusula ebubaka bino ku kyuma kino kyokka?</string>
<string name="deleteMessagesDescriptionEveryone">Oli mbanankubye kusula ebubaka bino byonna ku buli omu?</string> <string name="deleteMessagesDescriptionEveryone">Oli mbanankubye kusula ebubaka bino byonna ku buli omu?</string>
<string name="deleting">Okuggya</string> <string name="deleting">Okuggya</string>

View File

@ -136,13 +136,11 @@
<string name="deleteAfterGroupPR1BlockUser">ຫ້າມຜູ້ນັກ</string> <string name="deleteAfterGroupPR1BlockUser">ຫ້າມຜູ້ນັກ</string>
<string name="deleteAfterGroupPR3GroupErrorLeave">ບໍ່ສາມາດອອກໄດ້ໃນຂະນະນີ້ທ່ານກໍາລັງເພີ່ມຫຼຶລົບສະມາຊິກ.</string> <string name="deleteAfterGroupPR3GroupErrorLeave">ບໍ່ສາມາດອອກໄດ້ໃນຂະນະນີ້ທ່ານກໍາລັງເພີ່ມຫຼຶລົບສະມາຊິກ.</string>
<string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b>ໄດ້ກຳນົດລະຍະເວລາໃນການຂໍແກ້ຂອງຂໍ້ຄາບຊ່ວງທ່ານ<b>{time}</b></string> <string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b>ໄດ້ກຳນົດລະຍະເວລາໃນການຂໍແກ້ຂອງຂໍ້ຄາບຊ່ວງທ່ານ<b>{time}</b></string>
<string name="deleteMessageConfirm">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມນີ້?</string>
<string name="deleteMessageDescriptionDevice">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມນີ້ຂອງຕ່າງອຸປະກອນນີ້ເທົ່ານັ້ນ?</string> <string name="deleteMessageDescriptionDevice">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມນີ້ຂອງຕ່າງອຸປະກອນນີ້ເທົ່ານັ້ນ?</string>
<string name="deleteMessageDescriptionEveryone">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມນີ້ກັບທຸກຄົນ?</string> <string name="deleteMessageDescriptionEveryone">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມນີ້ກັບທຸກຄົນ?</string>
<string name="deleteMessageDeviceOnly">ລຶບແຕ່ອຸປະກອນນີ້ເທົ່ານັ້ນ</string> <string name="deleteMessageDeviceOnly">ລຶບແຕ່ອຸປະກອນນີ້ເທົ່ານັ້ນ</string>
<string name="deleteMessageDevicesAll">ລຶບເທິງທຸກອຸປະກອນຂອງຂ້ອຍ</string> <string name="deleteMessageDevicesAll">ລຶບເທິງທຸກອຸປະກອນຂອງຂ້ອຍ</string>
<string name="deleteMessageEveryone">ລຶບໃຫ້ແກ່ທຸກຄົນ</string> <string name="deleteMessageEveryone">ລຶບໃຫ້ແກ່ທຸກຄົນ</string>
<string name="deleteMessagesConfirm">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມເຫົານີ້?</string>
<string name="deleteMessagesDescriptionDevice">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມເຫົານີ້ຂອງຕ່າງອຸປະກອນນີ້ເທົ່ານັ້ນ?</string> <string name="deleteMessagesDescriptionDevice">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມເຫົານີ້ຂອງຕ່າງອຸປະກອນນີ້ເທົ່ານັ້ນ?</string>
<string name="deleteMessagesDescriptionEveryone">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມເຫົານີ້ສຳລັບທຸກຄົນ?</string> <string name="deleteMessagesDescriptionEveryone">ທ່ານແນ່ໃຈບໍ່ວ່າທ່ານຕ້ອງການລຶບຂໍ້ຄວາມເຫົານີ້ສຳລັບທຸກຄົນ?</string>
<string name="deleting">ລາຍການລຶບ</string> <string name="deleting">ລາຍການລຶບ</string>

View File

@ -285,7 +285,6 @@
<item quantity="many">Ištrinti žinutes</item> <item quantity="many">Ištrinti žinutes</item>
<item quantity="other">Ištrinti žinutes</item> <item quantity="other">Ištrinti žinutes</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Ar tikrai norite ištrinti šią žinutę?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Žinutė ištrinta</item> <item quantity="one">Žinutė ištrinta</item>
<item quantity="few">Žinutės ištrintos</item> <item quantity="few">Žinutės ištrintos</item>
@ -305,7 +304,6 @@
<item quantity="many">Nepavyko ištrinti žinučių</item> <item quantity="many">Nepavyko ištrinti žinučių</item>
<item quantity="other">Nepavyko ištrinti žinučių</item> <item quantity="other">Nepavyko ištrinti žinučių</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Ar tikrai norite ištrinti šias žinutes?</string>
<string name="deleteMessagesDescriptionDevice">Ar tikrai norite ištrinti šias žinutes tik iš šio įrenginio?</string> <string name="deleteMessagesDescriptionDevice">Ar tikrai norite ištrinti šias žinutes tik iš šio įrenginio?</string>
<string name="deleteMessagesDescriptionEveryone">Ar tikrai norite ištrinti šias žinutes visiems?</string> <string name="deleteMessagesDescriptionEveryone">Ar tikrai norite ištrinti šias žinutes visiems?</string>
<string name="deleting">Ištrinama</string> <string name="deleting">Ištrinama</string>

View File

@ -266,7 +266,6 @@
<string name="deleteAfterLegacyDisappearingMessagesLegacy">Mantojums</string> <string name="deleteAfterLegacyDisappearingMessagesLegacy">Mantojums</string>
<string name="deleteAfterLegacyDisappearingMessagesOriginal">Gaistošo ziņojumu oriģinālā versija.</string> <string name="deleteAfterLegacyDisappearingMessagesOriginal">Gaistošo ziņojumu oriģinālā versija.</string>
<string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b> iestatīja pazūdošo ziņu taimeri uz <b>{time}</b></string> <string name="deleteAfterLegacyDisappearingMessagesTheyChangedTimer"><b>{name}</b> iestatīja pazūdošo ziņu taimeri uz <b>{time}</b></string>
<string name="deleteMessageConfirm">Vai jūs esat pārliecināti ka vēlaties dzēst šo ziņu?</string>
<string name="deleteMessageDeletedGlobally">Šis ziņojums tika izdzēsts</string> <string name="deleteMessageDeletedGlobally">Šis ziņojums tika izdzēsts</string>
<string name="deleteMessageDeletedLocally">Šis ziņojums tika izdzēsts šajā ierīcē</string> <string name="deleteMessageDeletedLocally">Šis ziņojums tika izdzēsts šajā ierīcē</string>
<string name="deleteMessageDescriptionDevice">Vai jūs esat pārliecināti ka vēlaties dzēst šo ziņu tikai no šīs ierīces?</string> <string name="deleteMessageDescriptionDevice">Vai jūs esat pārliecināti ka vēlaties dzēst šo ziņu tikai no šīs ierīces?</string>
@ -279,7 +278,6 @@
<item quantity="one">Neizdevās dzēst ziņu</item> <item quantity="one">Neizdevās dzēst ziņu</item>
<item quantity="other">Neizdevās dzēst ziņas</item> <item quantity="other">Neizdevās dzēst ziņas</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Vai jūs esat pārliecināti, ka vēlaties dzēst šos ziņojumus?</string>
<string name="deleteMessagesDescriptionDevice">Vai esat pārliecināts, ka vēlaties dzēst šos ziņojumus tikai no šīs ierīces?</string> <string name="deleteMessagesDescriptionDevice">Vai esat pārliecināts, ka vēlaties dzēst šos ziņojumus tikai no šīs ierīces?</string>
<string name="deleteMessagesDescriptionEveryone">Vai esat pārliecināts, ka vēlaties dzēst šos ziņojumus visiem?</string> <string name="deleteMessagesDescriptionEveryone">Vai esat pārliecināts, ka vēlaties dzēst šos ziņojumus visiem?</string>
<string name="deleting">Dzēšana</string> <string name="deleting">Dzēšana</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Избриши порака</item> <item quantity="one">Избриши порака</item>
<item quantity="other">Избриши пораки</item> <item quantity="other">Избриши пораки</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Дали сте сигурни дека сакате да ја избришете оваа порака?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Пораката е избришана</item> <item quantity="one">Пораката е избришана</item>
<item quantity="other">Пораките се избришани</item> <item quantity="other">Пораките се избришани</item>
@ -297,7 +296,6 @@
<item quantity="one">Не успеа да ја избришете пораката</item> <item quantity="one">Не успеа да ја избришете пораката</item>
<item quantity="other">Не успеа да ги избришете пораките</item> <item quantity="other">Не успеа да ги избришете пораките</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Дали сте сигурни дека сакате да ги избришете овие пораки?</string>
<string name="deleteMessagesDescriptionDevice">Дали сте сигурни дека сакате да ги избришете овие пораки само од овој уред?</string> <string name="deleteMessagesDescriptionDevice">Дали сте сигурни дека сакате да ги избришете овие пораки само од овој уред?</string>
<string name="deleteMessagesDescriptionEveryone">Дали сте сигурни дека сакате да ги избришете овие пораки за сите?</string> <string name="deleteMessagesDescriptionEveryone">Дали сте сигурни дека сакате да ги избришете овие пораки за сите?</string>
<string name="deleting">Бришење...</string> <string name="deleting">Бришење...</string>

View File

@ -282,7 +282,6 @@
<item quantity="one">Мессеж устгах</item> <item quantity="one">Мессеж устгах</item>
<item quantity="other">Мессежүүдийг устгах</item> <item quantity="other">Мессежүүдийг устгах</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Та энэ зурвасыг устгахдаа итгэлтэй байна уу?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Мессеж устгагдсан</item> <item quantity="one">Мессеж устгагдсан</item>
<item quantity="other">Мессежүүд устгагдсан</item> <item quantity="other">Мессежүүд устгагдсан</item>
@ -298,7 +297,6 @@
<item quantity="one">Мессеж устгах амжилтгүй боллоо</item> <item quantity="one">Мессеж устгах амжилтгүй боллоо</item>
<item quantity="other">Мессежүүд устгах амжилтгүй боллоо</item> <item quantity="other">Мессежүүд устгах амжилтгүй боллоо</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Та эдгээр зурвасуудыг устгахдаа итгэлтэй байна уу?</string>
<string name="deleteMessagesDescriptionDevice">Та эдгээр мессежүүдийг зөвхөн энэ төхөөрөмжөөс л устгахыг хүсэж байна уу?</string> <string name="deleteMessagesDescriptionDevice">Та эдгээр мессежүүдийг зөвхөн энэ төхөөрөмжөөс л устгахыг хүсэж байна уу?</string>
<string name="deleteMessagesDescriptionEveryone">Та эдгээр мессежүүдийг бүгдэд зориулж устгахыг хүсэж байна уу?</string> <string name="deleteMessagesDescriptionEveryone">Та эдгээр мессежүүдийг бүгдэд зориулж устгахыг хүсэж байна уу?</string>
<string name="deleting">Устгаж байна</string> <string name="deleting">Устгаж байна</string>

View File

@ -280,7 +280,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">Padam Mesej</item> <item quantity="other">Padam Mesej</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Adakah anda yakin anda mahu memadamkan mesej ini?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">Mesej dipadam</item> <item quantity="other">Mesej dipadam</item>
</plurals> </plurals>
@ -294,7 +293,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">Gagal untuk memadam mesej</item> <item quantity="other">Gagal untuk memadam mesej</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Adakah anda yakin anda mahu memadamkan mesej-mesej ini?</string>
<string name="deleteMessagesDescriptionDevice">Adakah anda pasti mahu memadamkan mesej ini daripada peranti ini sahaja?</string> <string name="deleteMessagesDescriptionDevice">Adakah anda pasti mahu memadamkan mesej ini daripada peranti ini sahaja?</string>
<string name="deleteMessagesDescriptionEveryone">Adakah anda pasti mahu memadamkan mesej ini untuk semua orang?</string> <string name="deleteMessagesDescriptionEveryone">Adakah anda pasti mahu memadamkan mesej ini untuk semua orang?</string>
<string name="deleting">Memadam</string> <string name="deleting">Memadam</string>

View File

@ -279,7 +279,6 @@
<plurals name="deleteMessage"> <plurals name="deleteMessage">
<item quantity="other">မက်ဆေ့ချ် ဖျက်မည်</item> <item quantity="other">မက်ဆေ့ချ် ဖျက်မည်</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">ဤမက်ဆေ့ချ်ကို ဖျက်လိုသည်မှာ သေချာပါသလား။</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="other">မက်ဆေ့ချ်များ ဖျက်ထားသည်</item> <item quantity="other">မက်ဆေ့ချ်များ ဖျက်ထားသည်</item>
</plurals> </plurals>
@ -293,7 +292,6 @@
<plurals name="deleteMessageFailed"> <plurals name="deleteMessageFailed">
<item quantity="other">မက်ဆေ့ခ်ျဖျက်ရန် မအောင်မြင်ပါ</item> <item quantity="other">မက်ဆေ့ခ်ျဖျက်ရန် မအောင်မြင်ပါ</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">ဤမက်ဆေ့ချ်များကို ဖျက်လိုသည်မှာ သေချာပါသလား။</string>
<string name="deleteMessagesDescriptionDevice">ဤစက်ကိရိယာ မှသာ မက်ဆေ့ချ်များဖျက်လိုပါသလား။</string> <string name="deleteMessagesDescriptionDevice">ဤစက်ကိရိယာ မှသာ မက်ဆေ့ချ်များဖျက်လိုပါသလား။</string>
<string name="deleteMessagesDescriptionEveryone">မက်ဆေ့ဂျ်တွေ အားလုံး ကိုဖျက်လိုပါသလား?</string> <string name="deleteMessagesDescriptionEveryone">မက်ဆေ့ဂျ်တွေ အားလုံး ကိုဖျက်လိုပါသလား?</string>
<string name="deleting">ဖျက်နေသည်</string> <string name="deleting">ဖျက်နေသည်</string>

View File

@ -281,7 +281,6 @@
<item quantity="one">Slett melding</item> <item quantity="one">Slett melding</item>
<item quantity="other">Slett meldinger</item> <item quantity="other">Slett meldinger</item>
</plurals> </plurals>
<string name="deleteMessageConfirm">Er du sikker på at du vil slette denne meldingen?</string>
<plurals name="deleteMessageDeleted"> <plurals name="deleteMessageDeleted">
<item quantity="one">Melding slettet</item> <item quantity="one">Melding slettet</item>
<item quantity="other">Meldinger slettet</item> <item quantity="other">Meldinger slettet</item>
@ -297,7 +296,6 @@
<item quantity="one">Kunne ikke slette meldingen</item> <item quantity="one">Kunne ikke slette meldingen</item>
<item quantity="other">Kunne ikke slette meldinger</item> <item quantity="other">Kunne ikke slette meldinger</item>
</plurals> </plurals>
<string name="deleteMessagesConfirm">Er du sikker på at du vil slette disse meldingene?</string>
<string name="deleteMessagesDescriptionDevice">Er du sikker på at du vil slette disse meldingene fra bare denne enheten?</string> <string name="deleteMessagesDescriptionDevice">Er du sikker på at du vil slette disse meldingene fra bare denne enheten?</string>
<string name="deleteMessagesDescriptionEveryone">Er du sikker på at du vil slette disse meldingene for alle?</string> <string name="deleteMessagesDescriptionEveryone">Er du sikker på at du vil slette disse meldingene for alle?</string>
<string name="deleting">Sletter</string> <string name="deleting">Sletter</string>

Some files were not shown because too many files have changed in this diff Show More