Feature/standardised message deletion (#1685)

* Initial commit with high level structure for new message deletion logic

* Adding admin logic

* New dialog styles

* Matching existing dialog closer to new designs

* Using the theme attribute danger instead of a hardcoded colour

* Using classes for the dialogs

Also cleaned up older references to align with newer look

* Adding cancel handling

Cleaning unused code

* Handling local deletion with batch message deletion

* Reusing the 'delete locally'

* Delete on device should "marl the message as deleted", not remove it from the db directly

* Displaying "marked as deleted" messages

Split the `BASE_DELETED_TYPE` into two types:
BASE_DELETED_OUTGOING_TYPE and BASE_DELETED_INCOMING_TYPE
so we can differentiate them visually.

* Proper handling of merged code

* Removed temp bg color

* Making sure the deleted message view is visible

* Renaming functions for clarity

* Adding the ability to customise the text for the deleted control messages

* Removing code that was added back from merging dev back in

* Using the updated strings

* Toast confirmation on 'delete locally'

* Recreating xml dialogs in Compose and moved logic in VM

* Removing hardcoded strings

* Updated message deletion logic

Still need to finalise "note to self" and "legacy groups"

* Deletion logic rework

Moving away from promises

* More deletion logic

Hndling unsend request retrieval as per figma docs

* Making sure multi-select works as expectec

* Multi message handling

Sharing admin logic

* Deleting reactions when deleting a message

* Deleting reactions when deleting a message

* Grabbing server hash from notification data

* Fixed unit tests

* Handling deletion od "marked as deleted" messages

* Handling Control Messages longpress and deletion

* Back up handling of no map data for huawei notifications

Also rethemed the send buttona dn home plus button to have better ax contrast by standardising the colour displayed on the accent color to be the same as the one on the sent messages

* Removed test line

* Reworking the deletion dialogs

We removed the 'delete locally' dialog, instead we show the 'delete for everyone' with the second option disabled

* Outgoing messages can all be marked as 'delete for everyone'

Cleaned up invisible copy button on black bgs

* PR feedback

* Updated huawei file and tested notifications

* Fixed SES-2802

Only force the priority to visible when going from not approved to approved

* Syncing state diaplays as sent

Syncing happens in the bg so the user doesn't need to know of it hence the status can display as "Sent" during the syncing phase.
Resyncing, in case it happens, can display the "Syncing" status as it would happen after a syncing error.

* Latest strings

---------

Co-authored-by: ThomasArtProcessors <71994342+ThomasArtProcessors@users.noreply.github.com>
This commit is contained in:
ThomasSession 2024-10-14 15:33:11 +11:00 committed by GitHub
parent ecfa5d346a
commit 54ef260aa9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
136 changed files with 1467 additions and 732 deletions

View File

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

View File

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

View File

@ -24,6 +24,8 @@ class HuaweiTokenFetcher @Inject constructor(
override suspend fun fetch(): String? = HmsInstanceId.getInstance(context).run {
// 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.
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.libsignal.utilities.Log;
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.loaders.PagingMediaLoader;
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.session.libsession.database.MessageDataProvider
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.sending_receiving.attachments.Attachment
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) {
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseComponent.get(context).smsDatabase()
else DatabaseComponent.get(context).mmsDatabase()
@ -215,18 +215,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
}
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
override fun markMessageAsDeleted(timestamp: Long, author: String, displayedMessage: String) {
val database = DatabaseComponent.get(context).mmsSmsDatabase()
val address = Address.fromSerialized(author)
val message = database.getMessageFor(timestamp, address) ?: return null
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
else DatabaseComponent.get(context).smsDatabase()
messagingDatabase.markAsDeleted(message.id, message.isRead, message.hasMention)
if (message.isOutgoing) {
messagingDatabase.deleteMessage(message.id)
}
val message = database.getMessageFor(timestamp, address) ?: return Log.w("", "Failed to find message to mark as deleted")
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? =

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
/**
* 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.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
@ -110,6 +109,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey
import org.thoughtcrime.securesms.conversation.ConversationActionBarDelegate
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.OnReactionSelectedListener
import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.MESSAGE_TIMESTAMP
@ -131,6 +131,7 @@ import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate
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.VisibleMessageViewDelegate
import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar
@ -173,8 +174,6 @@ import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
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.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
@ -244,8 +243,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
.get(LinkPreviewViewModel::class.java)
}
private var openLinkDialogUrl: String? by mutableStateOf(null)
private val threadId: Long by lazy {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
@ -348,9 +345,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (!viewModel.isMessageRequestThread &&
viewModel.canReactToMessages
) {
showEmojiPicker(message, view)
showConversationReaction(message, view)
} else {
handleLongPress(message, position)
selectMessage(message, position)
}
},
onDeselect = { message, position ->
@ -410,7 +407,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// endregion
fun showOpenUrlDialog(url: String){
openLinkDialogUrl = url
viewModel.onCommand(ShowOpenUrlDialog(url))
}
// region Lifecycle
@ -423,16 +420,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.dialogOpenUrl.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SessionMaterialTheme {
if(!openLinkDialogUrl.isNullOrEmpty()){
OpenURLAlertDialog(
url = openLinkDialogUrl!!,
onDismissRequest = {
openLinkDialogUrl = null
}
)
}
}
val dialogsState by viewModel.dialogsState.collectAsState()
ConversationV2Dialogs(
dialogsState = dialogsState,
sendCommand = viewModel::onCommand
)
}
}
@ -442,7 +434,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val recipient = viewModel.recipient
val openGroup = recipient.let { viewModel.openGroup }
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()
}
@ -659,6 +651,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
recyclerScrollState = newState
}
})
lifecycleScope.launch {
viewModel.isAdmin.collect{
adapter.isAdmin = it
}
}
}
private fun scrollToMostRecentMessageIfWeShould() {
@ -856,7 +854,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
finish()
}
// show or hide the text input
binding.inputBar.isGone = uiState.hideInputBar
// show or hide loading indicator
binding.loader.isVisible = uiState.showLoader
}
}
}
@ -1291,7 +1293,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
// `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 actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this)
actionModeCallback.delegate = this
@ -1309,15 +1311,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 {
visibleMessageView.messageContentView.drawToBitmap()
messageContentView.drawToBitmap()
} catch (e: Exception) {
Log.e("Loki", "Failed to show emoji picker", e)
return
}
emojiPickerVisible = true
ViewUtil.hideKeyboard(this, visibleMessageView)
ViewUtil.hideKeyboard(this, messageView)
binding.reactionsShade.isVisible = true
binding.scrollToBottomButton.isVisible = false
binding.conversationRecyclerView.suppressLayout(true)
@ -1339,14 +1347,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(
messageContentBitmap,
topLeft[0].toFloat(),
topLeft[1].toFloat(),
visibleMessageView.messageContentView.width,
messageContentView.width,
message.isOutgoing,
visibleMessageView.messageContentView
messageContentView
)
reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey)
}
@ -2066,88 +2074,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
}
override fun selectMessages(messages: Set<MessageRecord>) {
handleLongPress(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)
}
selectMessage(messages.first(), 0) //TODO: begin selection mode
}
// Note: The messages in the provided set may be a single message, or multiple if there are a
// group of selected messages.
override fun deleteMessages(messages: Set<MessageRecord>) {
val recipient = viewModel.recipient
if (recipient == null) {
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 && 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)
}
}
viewModel.handleMessagesDeletion(messages)
endActionMode()
}
override fun banUser(messages: Set<MessageRecord>) {

View File

@ -5,6 +5,7 @@ import android.database.Cursor
import android.util.SparseArray
import android.util.SparseBooleanArray
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault
@ -35,7 +36,7 @@ class ConversationAdapter(
private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> 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 onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
private val glide: RequestManager,
@ -44,6 +45,7 @@ class ConversationAdapter(
private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() }
private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() }
var selectedItems = mutableSetOf<MessageRecord>()
var isAdmin: Boolean = false
private var searchQuery: String? = null
var visibleMessageViewDelegate: VisibleMessageViewDelegate? = null
@ -155,12 +157,18 @@ class ConversationAdapter(
} else {
visibleMessageView.onPress = 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 -> {
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.core.content.ContextCompat
import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint
@ -222,7 +223,6 @@ class ConversationReactionOverlay : FrameLayout {
endScale = spaceAvailableForItem / conversationItemSnapshot.height
endX += Util.halfOffsetFromScale(conversationItemSnapshot.width, endScale) * if (isMessageOnLeft) -1 else 1
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);
endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.height, endScale)
} else {
@ -271,11 +271,17 @@ class ConversationReactionOverlay : FrameLayout {
revealAnimatorSet.start()
if (isWideLayout) {
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())
} else {
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
contextMenu.show(offsetX.toInt(), (menuTop + menuPadding).toInt())
}
@ -526,19 +532,30 @@ class ConversationReactionOverlay : FrameLayout {
val recipient = get(context).threadDatabase().getRecipientForThreadId(message.threadId)
?: return emptyList()
val userPublicKey = getLocalNumber(context)!!
// control messages and "marked as deleted" messages can only delete
val isDeleteOnly = message.isDeleted || message.isControlMessage
// 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
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)
}
// Copy message text
if (!containsControlMessage && hasText) {
if (!containsControlMessage && hasText && !isDeleteOnly) {
items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
}
// 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) })
}
// Delete message
@ -547,15 +564,20 @@ class ConversationReactionOverlay : FrameLayout {
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
}
// 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) })
}
// 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) })
}
// 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
if (message.isFailed) {
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) })
}
// 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.
val mmsMessage = message as MediaMmsMessageRecord
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
}

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,6 +1,8 @@
package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import android.content.Context
import android.widget.Toast
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@ -10,13 +12,14 @@ import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.R
import org.session.libsession.database.MessageDataProvider
import org.session.libsession.messaging.messages.ExpirationConfiguration
import org.session.libsession.messaging.open_groups.OpenGroup
@ -25,25 +28,32 @@ import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAt
import org.session.libsession.messaging.utilities.AccountId
import org.session.libsession.messaging.utilities.SodiumUtilities
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.MessageType
import org.session.libsession.utilities.recipients.getType
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
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.MmsMessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.mms.AudioSlide
import org.thoughtcrime.securesms.repository.ConversationRepository
import java.util.UUID
class ConversationViewModel(
val threadId: Long,
val edKeyPair: KeyPair?,
private val application: Application,
private val repository: ConversationRepository,
private val storage: Storage,
private val messageDataProvider: MessageDataProvider,
database: MmsDatabase,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
) : ViewModel() {
val showSendAfterApprovalText: Boolean
@ -52,8 +62,44 @@ class ConversationViewModel(
private val _uiState = MutableStateFlow(ConversationUiState(conversationExists = true))
val uiState: StateFlow<ConversationUiState> = _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 {
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?
get() = storage.getExpirationConfiguration(threadId)
@ -180,18 +226,394 @@ class ConversationViewModel(
repository.deleteThread(threadId)
}
fun deleteLocally(message: MessageRecord) {
stopPlayingAudioMessage(message)
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for delete locally action")
repository.deleteLocally(recipient, message)
fun handleMessagesDeletion(messages: Set<MessageRecord>){
val conversation = recipient
if (conversation == null) {
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 } && (
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, remove it from the db instead
if(messages.all { it.isDeleted }){
// 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.
*/
private fun stopPlayingAudioMessage(message: MessageRecord) {
private fun stopMessageAudio(message: MessageRecord) {
val mmsMessage = message as? MmsMessageRecord ?: return
val audioSlide = mmsMessage.slideDeck.audioSlide ?: return
stopMessageAudio(audioSlide)
}
private fun stopMessageAudio(audioSlide: AudioSlide) {
AudioSlidePlayer.getInstance()?.takeIf { it.audioSlide == audioSlide }?.stop()
}
@ -200,35 +622,13 @@ class ConversationViewModel(
repository.setApproved(recipient, true)
}
fun deleteForEveryone(message: MessageRecord) = viewModelScope.launch {
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
stopPlayingAudioMessage(message)
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 {
repository.banUser(threadId, recipient)
.onSuccess {
showMessage("Successfully banned user")
showMessage(application.getString(R.string.banUserBanned))
}
.onFailure {
showMessage("Couldn't ban user due to error: $it")
showMessage(application.getString(R.string.banErrorFailed))
}
}
@ -237,13 +637,13 @@ class ConversationViewModel(
repository.banAndDeleteAll(threadId, messageRecord.individualRecipient)
.onSuccess {
// 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.
repository.deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord)
}
.onFailure {
showMessage("Couldn't execute request due to error: $it")
showMessage(application.getString(R.string.banErrorFailed))
}
}
@ -256,7 +656,7 @@ class ConversationViewModel(
}
}
.onFailure {
showMessage("Couldn't accept message request due to error: $it")
Log.w("", "Failed to accept message request: $it")
}
}
@ -306,6 +706,40 @@ class ConversationViewModel(
attachmentDownloadHandler.onAttachmentDownloadRequest(attachment)
}
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
interface AssistedFactory {
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
@ -315,23 +749,50 @@ class ConversationViewModel(
class Factory @AssistedInject constructor(
@Assisted private val threadId: Long,
@Assisted private val edKeyPair: KeyPair?,
private val application: Application,
private val repository: ConversationRepository,
private val storage: Storage,
private val mmsDatabase: MmsDatabase,
private val messageDataProvider: MessageDataProvider,
private val lokiMessageDb: LokiMessageDatabase,
private val textSecurePreferences: TextSecurePreferences
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ConversationViewModel(
threadId = threadId,
edKeyPair = edKeyPair,
application = application,
repository = repository,
storage = storage,
messageDataProvider = messageDataProvider,
database = mmsDatabase
lokiMessageDb = lokiMessageDb,
textSecurePreferences = textSecurePreferences
) 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)
@ -340,7 +801,8 @@ data class ConversationUiState(
val uiMessages: List<UiMessage> = emptyList(),
val isMessageRequestAccepted: Boolean? = null,
val conversationExists: Boolean,
val hideInputBar: Boolean = false
val hideInputBar: Boolean = false,
val showLoader: Boolean = false
)
data class RetrieveOnce<T>(val retrieval: () -> T?) {

View File

@ -77,7 +77,9 @@ class InputBarButton : RelativeLayout {
result.layoutParams = LayoutParams(size, size)
result.scaleType = ImageView.ScaleType.CENTER_INSIDE
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
}

View File

@ -10,6 +10,7 @@ import org.session.libsession.messaging.utilities.AccountId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
@ -43,6 +44,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
// Embedded function
fun userCanDeleteSelectedItems(): Boolean {
// admin can delete all combinations
if(adapter.isAdmin) return true
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
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 {
val selectedItems = adapter.selectedItems
val selectedItems = adapter.selectedItems.toSet()
when (item.itemId) {
R.id.menu_context_delete_message -> delegate?.deleteMessages(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.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
@ -54,11 +55,13 @@ class ControlMessageView : LinearLayout {
@Inject lateinit var disappearingMessages: DisappearingMessages
val controlContentView: View get() = binding.controlContentView
init {
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.iconImageView.isGone = true
binding.expirationTimerView.isGone = true
@ -84,7 +87,7 @@ class ControlMessageView : LinearLayout {
&& message.expiryMode != (MessagingModuleConfiguration.shared.storage.getExpirationConfiguration(message.threadId)?.expiryMode ?: ExpiryMode.NONE)
&& threadRecipient?.isGroupRecipient != true
followSetting.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
binding.controlContentView.setOnClickListener { disappearingMessages.showFollowSettingDialog(context, message) }
}
}
message.isMediaSavedNotification -> {
@ -128,7 +131,7 @@ class ControlMessageView : LinearLayout {
}
// remove clicks by default
setOnClickListener(null)
binding.controlContentView.setOnClickListener(null)
hideInfo()
// handle click behaviour depending on criteria
@ -138,7 +141,7 @@ class ControlMessageView : LinearLayout {
// show a dedicated privacy dialog
!TextSecurePreferences.isCallNotificationsEnabled(context) -> {
showInfo()
setOnClickListener {
binding.controlContentView.setOnClickListener {
context.showSessionDialog {
val titleTxt = context.getSubbedString(
R.string.callsMissedCallFrom,
@ -165,7 +168,7 @@ class ControlMessageView : LinearLayout {
// show a dedicated permission dialog
!Permissions.hasAll(context, Manifest.permission.RECORD_AUDIO) -> {
showInfo()
setOnClickListener {
binding.controlContentView.setOnClickListener {
context.showSessionDialog {
val titleTxt = context.getSubbedString(
R.string.callsMissedCallFrom,
@ -199,6 +202,14 @@ class ControlMessageView : LinearLayout {
binding.textView.isGone = 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(){

View File

@ -21,7 +21,8 @@ class DeletedMessageView : LinearLayout {
// region Updating
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
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.deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
}

View File

@ -71,7 +71,6 @@ class VisibleMessageContentView : ConstraintLayout {
binding.contentParent.mainColor = color
binding.contentParent.cornerRadius = resources.getDimension(R.dimen.message_corner_radius)
val onlyBodyMessage = message is SmsMessageRecord
val mediaThumbnailMessage = contactIsTrusted && message is MmsMessageRecord && message.slideDeck.thumbnailSlide != null
// reset visibilities / containers
@ -80,6 +79,7 @@ class VisibleMessageContentView : ConstraintLayout {
onContentDoubleTap = null
if (message.isDeleted) {
binding.contentParent.isVisible = true
binding.deletedMessageView.root.isVisible = true
binding.deletedMessageView.root.bind(message, getTextColor(context, message))
binding.bodyTextView.isVisible = false

View File

@ -280,13 +280,12 @@ class VisibleMessageView : FrameLayout {
// 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.).
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.
// Also: We set all delivery status elements visibility to false just to make sure we don't display any
// stale data.
if (textId == null) return
val messageStatus = getMessageStatusInfo(message) ?: return
binding.messageInnerLayout.modifyLayoutParams<FrameLayout.LayoutParams> {
gravity = if (message.isOutgoing) Gravity.END else Gravity.START
@ -295,16 +294,17 @@ class VisibleMessageView : FrameLayout {
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
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
// about are: isFailed, isSyncFailed, isPending, isSyncing, isResyncing, isRead, and isSent.
textId.let(binding.messageStatusTextView::setText)
iconColor?.let(binding.messageStatusTextView::setTextColor)
iconID?.let { ContextCompat.getDrawable(context, it) }
?.run { iconColor?.let { mutate().apply { setTint(it) } } ?: this }
messageStatus.messageText?.let(binding.messageStatusTextView::setText)
messageStatus.iconTint?.let(binding.messageStatusTextView::setTextColor)
messageStatus.iconId?.let { ContextCompat.getDrawable(context, it) }
?.run { messageStatus.iconTint?.let { mutate().apply { setTint(it) } } ?: this }
?.let(binding.messageStatusImageView::setImageDrawable)
// Potential options at this point are that the message is:
@ -381,7 +381,7 @@ class VisibleMessageView : FrameLayout {
@ColorInt val iconTint: Int?,
@StringRes val messageText: Int?)
private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo = when {
private fun getMessageStatusInfo(message: MessageRecord): MessageStatusInfo? = when {
message.isFailed ->
MessageStatusInfo(R.drawable.ic_delivery_status_failed,
getThemedColor(context, R.attr.danger),
@ -410,7 +410,7 @@ class VisibleMessageView : FrameLayout {
)
}
}
message.isSyncing || message.isResyncing ->
message.isResyncing ->
MessageStatusInfo(
R.drawable.ic_delivery_status_sending,
context.getColorFromAttr(R.attr.message_status_color),
@ -422,16 +422,21 @@ class VisibleMessageView : FrameLayout {
context.getColorFromAttr(R.attr.message_status_color),
R.string.read
)
message.isSent ->
message.isSyncing || message.isSent -> // syncing should happen silently in the bg so we can mark it as sent
MessageStatusInfo(
R.drawable.ic_delivery_status_sent,
context.getColorFromAttr(R.attr.message_status_color),
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 -> {
// 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).
MessageStatusInfo(null, null, null)
null
}
}
@ -480,10 +485,13 @@ class VisibleMessageView : FrameLayout {
// region Interaction
@SuppressLint("ClickableViewAccessibility")
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) {
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_UP -> onUp(event)
}

View File

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

View File

@ -304,18 +304,21 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
}
override fun markAsDeleted(messageId: Long, read: Boolean, hasMention: Boolean) {
override fun markAsDeleted(messageId: Long, isOutgoing: Boolean, displayedMessage: String) {
val database = databaseHelper.writableDatabase
val contentValues = ContentValues()
contentValues.put(READ, 1)
contentValues.put(BODY, "")
contentValues.put(BODY, displayedMessage)
contentValues.put(HAS_MENTION, 0)
database.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
val attachmentDatabase = get(context).attachmentDatabase()
queue(Runnable { attachmentDatabase.deleteAttachmentsForMessage(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) {

View File

@ -42,6 +42,7 @@ public interface MmsSmsColumns {
protected static final long JOINED_TYPE = 4;
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_OUTBOX_TYPE = 21;
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_INSECURE_SMS_FALLBACK = 26;
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_RESYNCING_TYPE = 30;
protected static final long BASE_SYNC_FAILED_TYPE = 31;
@ -61,6 +62,7 @@ public interface MmsSmsColumns {
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
BASE_PENDING_SECURE_SMS_FALLBACK,
BASE_PENDING_INSECURE_SMS_FALLBACK,
BASE_DELETED_OUTGOING_TYPE,
OUTGOING_CALL_TYPE};
@ -182,7 +184,9 @@ public interface MmsSmsColumns {
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) {
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()
try {
writableDatabase.delete(TABLE_NAME, query, args)
@ -174,7 +174,54 @@ class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database
} finally {
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 {
val query = "$MESSAGE_ID = ? AND $IS_MMS = ?"

View File

@ -237,14 +237,17 @@ public class SmsDatabase extends MessagingDatabase {
}
@Override
public void markAsDeleted(long messageId, boolean read, boolean hasMention) {
public void markAsDeleted(long messageId, boolean isOutgoing, String displayedMessage) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
contentValues.put(READ, 1);
contentValues.put(BODY, "");
contentValues.put(BODY, displayedMessage);
contentValues.put(HAS_MENTION, 0);
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

View File

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.R
import java.security.MessageDigest
import network.loki.messenger.libsession_util.ConfigBase
import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_HIDDEN
@ -74,6 +73,8 @@ import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Co
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
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.ECKeyPair
@ -758,6 +759,12 @@ open class Storage(
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(
messageID: Long,
isMms: Boolean,
@ -1728,6 +1735,12 @@ open class Storage(
DatabaseComponent.get(context).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) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
recipientDb.setBlocked(recipients, isBlocked)

View File

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

View File

@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.notifications
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat.getString
@ -30,27 +33,55 @@ private const val TAG = "PushHandler"
class PushReceiver @Inject constructor(@ApplicationContext val context: Context) {
private val json = Json { ignoreUnknownKeys = true }
fun onPush(dataMap: Map<String, String>?) {
onPush(dataMap?.asByteArray())
/**
* Both push services should hit this method once they receive notification data
* As long as it is properly formatted
*/
fun onPushDataReceived(dataMap: Map<String, String>?) {
addMessageReceiveJob(dataMap?.asPushData())
}
fun onPush(data: ByteArray?) {
if (data == null) {
onPush()
/**
* 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
}
try {
val envelopeAsData = MessageWrapper.unwrap(data).toByteArray()
val job = BatchMessageReceiveJob(listOf(MessageReceiveParameters(envelopeAsData)), null)
val envelopeAsData = MessageWrapper.unwrap(pushData.data).toByteArray()
val job = BatchMessageReceiveJob(listOf(
MessageReceiveParameters(
data = envelopeAsData,
serverHash = pushData.metadata?.msg_hash
)
), null)
JobQueue.shared.add(job)
} catch (e: Exception) {
Log.d(TAG, "Failed to unwrap data for message due to error.", e)
}
}
private fun onPush() {
private fun sendGenericNotification() {
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)
.setSmallIcon(R.drawable.ic_notification)
.setColor(context.getColor(R.color.textsecure_primary))
@ -61,10 +92,11 @@ class PushReceiver @Inject constructor(@ApplicationContext val context: Context)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
NotificationManagerCompat.from(context).notify(11111, builder.build())
}
private fun Map<String, String>.asByteArray() =
private fun Map<String, String>.asPushData(): PushData =
when {
// this is a v2 push notification
containsKey("spns") -> {
@ -72,14 +104,14 @@ class PushReceiver @Inject constructor(@ApplicationContext val context: Context)
decrypt(Base64.decode(this["enc_payload"]))
} catch (e: Exception) {
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
else -> this["ENCRYPTED_DATA"]?.let(Base64::decode)
else -> PushData(this["ENCRYPTED_DATA"]?.let(Base64::decode), null)
}
private fun decrypt(encPayload: ByteArray): ByteArray? {
private fun decrypt(encPayload: ByteArray): PushData {
Log.d(TAG, "decrypt() called")
val encKey = getOrCreateNotificationKey()
@ -95,9 +127,12 @@ class PushReceiver @Inject constructor(@ApplicationContext val context: Context)
val metadataJson = (expectedList[0] as? BencodeString)?.value ?: error("no metadata")
val metadata: PushNotificationMetadata = json.decodeFromString(String(metadataJson))
return (expectedList.getOrNull(1) as? BencodeString)?.value.also {
// null content is valid only if we got a "data_too_long" flag
it?.let { check(metadata.data_len == it.size) { "wrong message data size" } }
return PushData(
data = (expectedList.getOrNull(1) as? BencodeString)?.value,
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" }
}
}
@ -115,4 +150,9 @@ class PushReceiver @Inject constructor(@ApplicationContext val context: Context)
)
)
}
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) {
withContext(Main) {
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 ->

View File

@ -1,18 +1,16 @@
package org.thoughtcrime.securesms.repository
import network.loki.messenger.libsession_util.util.ExpiryMode
import android.content.ContentResolver
import android.content.Context
import app.cash.copper.Query
import app.cash.copper.flow.observeQuery
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.database.MessageDataProvider
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.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
@ -21,12 +19,11 @@ import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.database.DatabaseContentProviders
import org.thoughtcrime.securesms.database.DraftDatabase
import org.thoughtcrime.securesms.database.ExpirationConfigurationDatabase
@ -44,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
interface ConversationRepository {
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
@ -55,18 +54,35 @@ interface ConversationRepository {
fun clearDrafts(threadId: Long)
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
fun setBlocked(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 setApproved(recipient: Recipient, isApproved: Boolean)
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): Result<Unit>
suspend fun deleteCommunityMessagesRemotely(threadId: Long, messages: Set<MessageRecord>)
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 deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<MessageRecord>): Result<Unit>
suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
suspend fun deleteThread(threadId: Long): Result<Unit>
suspend fun deleteMessageRequest(thread: ThreadRecord): Result<Unit>
suspend fun clearAllMessageRequests(block: Boolean): Result<Unit>
suspend fun acceptMessageRequest(threadId: Long, recipient: Recipient): Result<Unit>
fun declineMessageRequest(threadId: Long)
fun hasReceived(threadId: Long): Boolean
}
@ -158,13 +174,57 @@ class DefaultConversationRepository @Inject constructor(
storage.setBlocked(listOf(recipient), blocked)
}
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
textSecurePreferences.getLocalNumber()?.let {
MessageSender.send(unsendRequest, Address.fromSerialized(it))
}
/**
* This will delete these messages from the db
* Not to be confused with 'marking messages as deleted'
*/
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)
}
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)
}
messageDataProvider.deleteMessage(message.id, !message.isMms)
}
override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
@ -180,59 +240,82 @@ class DefaultConversationRepository @Inject constructor(
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,
recipient: Recipient,
message: MessageRecord
): Result<Unit> = suspendCoroutine { continuation ->
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
messages: Set<MessageRecord>
) {
// delete the messages remotely
val publicKey = recipient.address.serialize()
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) {
val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(Result.success(Unit))
}.fail { error ->
Log.w("TAG", "Call to OpenGroupApi.deleteForEveryone failed - attempting to resume..")
continuation.resume(Result.failure(error))
}
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) }
}
// 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.
if (serverId == null) {
Log.w("ConversationRepository","Found community message without a server ID - deleting locally.")
// send an UnsendRequest to recipient's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
MessageSender.send(unsendRequest, recipient.address)
}
}
}
// Caution: The bool returned from `deleteMessage` is NOT "Was the message
// successfully deleted?" - it is "Was the thread itself also deleted because
// removing that message resulted in an empty thread?".
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
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)
}
}
}
else // If this thread is NOT in a Community
{
messageDataProvider.deleteMessage(message.id, !message.isMms)
messageDataProvider.getServerHashForMessage(message.id, message.isMms)?.let { serverHash ->
var publicKey = recipient.address.serialize()
if (recipient.isClosedGroupRecipient) {
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
}
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))
}
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
.success {
continuation.resume(Result.success(Unit))
}.fail { error ->
Log.w("ConversationRepository", "Call to SnodeAPI.deleteMessage failed - attempting to resume..")
continuation.resume(Result.failure(error))
}
// send an UnsendRequest to user's swarm
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
userAddress?.let { MessageSender.send(unsendRequest, it) }
}
}
}
@ -246,38 +329,6 @@ class DefaultConversationRepository @Inject constructor(
)
}
override suspend fun deleteMessageWithoutUnsendRequest(
threadId: Long,
messages: Set<MessageRecord>
): Result<Unit> = suspendCoroutine { continuation ->
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)
.success {
messageDataProvider.deleteMessage(message.id, !message.isMms)
}.fail { error ->
continuation.resume(Result.failure(error))
}
}
} else {
for (message in messages) {
if (message.isMms) {
mmsDb.deleteMessage(message.id)
} else {
smsDb.deleteMessage(message.id)
}
}
}
continuation.resume(Result.success(Unit))
}
override suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> =
suspendCoroutine { continuation ->
val accountID = recipient.address.toString()

View File

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

View File

@ -19,6 +19,7 @@ interface ThemeColors {
val isLight: Boolean
val primary: Color
val danger: Color
val warning: Color
val disabled: Color
val background: Color
val backgroundSecondary: Color
@ -100,6 +101,7 @@ fun dangerButtonColors() = ButtonDefaults.buttonColors(
data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors {
override val isLight = false
override val danger = dangerDark
override val warning = primaryOrange
override val disabled = disabledDark
override val background = classicDark0
override val backgroundSecondary = classicDark1
@ -118,6 +120,7 @@ data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors
data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColors {
override val isLight = true
override val danger = dangerLight
override val warning = primaryOrange
override val disabled = disabledLight
override val background = classicLight6
override val backgroundSecondary = classicLight5
@ -136,6 +139,7 @@ data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColor
data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
override val isLight = false
override val danger = dangerDark
override val warning = primaryOrange
override val disabled = disabledDark
override val background = oceanDark2
override val backgroundSecondary = oceanDark1
@ -154,6 +158,7 @@ data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
data class OceanLight(override val primary: Color = primaryBlue) : ThemeColors {
override val isLight = true
override val danger = dangerLight
override val warning = primaryOrange
override val disabled = disabledLight
override val background = oceanLight7
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:height="24dp"
android:viewportWidth="50"
android:viewportHeight="50">
android:viewportHeight="50"
android:tint="?attr/colorControlNormal">
<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:fillColor="#000000"/>
android:fillColor="#FFFFFF"/>
<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:fillColor="#000000"/>
android:fillColor="#FFFFFF"/>
</vector>

View File

@ -10,4 +10,5 @@
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: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:fillColor="#00000000"
android:strokeColor="?textColorAlert"/>
</vector>
</vector>

View File

@ -351,4 +351,23 @@
android:layout_width="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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,8 +41,6 @@
<attr name="conversation_editor_text_color" format="reference|color"/>
<attr name="conversation_input_background" 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_emoji_toggle" format="reference"/>
<attr name="conversation_sticker_toggle" format="reference"/>

View File

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

View File

@ -21,7 +21,6 @@
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.6</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="alertDialogTheme">@style/ThemeOverlay.Session.AlertDialog</item>
<item name="conversationMenuSearchTintColor">?android:textColorPrimary</item>
@ -173,8 +172,6 @@
<item name="conversation_editor_background">#22ffffff</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_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_attach_camera">@drawable/ic_photo_camera_dark</item>
<item name="conversation_attach_image">@drawable/ic_image_dark</item>

View File

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

View File

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.conversation.v2
import android.app.Application
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
@ -12,7 +13,6 @@ import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.anySet
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
@ -28,7 +28,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private val repository = mock<ConversationRepository>()
private val storage = mock<Storage>()
private val mmsDatabase = mock<MmsDatabase>()
private val application = mock<Application>()
private val threadId = 123L
private val edKeyPair = mock<KeyPair>()
@ -36,7 +36,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
private lateinit var messageRecord: MessageRecord
private val viewModel: ConversationViewModel by lazy {
ConversationViewModel(threadId, edKeyPair, repository, storage, mock(), mmsDatabase)
ConversationViewModel(threadId, edKeyPair, application, repository, storage, mock(), mock(), mock())
}
@Before
@ -88,59 +88,27 @@ class ConversationViewModelTest: BaseViewModelTest() {
verify(repository).setBlocked(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
fun `should emit error message on ban user failure`() = runBlockingTest {
val error = Throwable()
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.failure(error))
whenever(application.getString(any())).thenReturn("Ban failed")
viewModel.banUser(recipient)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
assertThat(viewModel.uiState.first().uiMessages.first().message, equalTo("Ban failed"))
}
@Test
fun `should emit a message on ban user success`() = runBlockingTest {
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banUser(recipient)
assertThat(
viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user")
equalTo("User banned")
)
}
@ -148,21 +116,23 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should emit error message on ban user and delete all failure`() = runBlockingTest {
val error = Throwable()
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.failure(error))
whenever(application.getString(any())).thenReturn("Ban failed")
viewModel.banAndDeleteAll(messageRecord)
assertThat(viewModel.uiState.first().uiMessages.first().message, endsWith("$error"))
assertThat(viewModel.uiState.first().uiMessages.first().message, equalTo("Ban failed"))
}
@Test
fun `should emit a message on ban user and delete all success`() = runBlockingTest {
whenever(repository.banAndDeleteAll(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banAndDeleteAll(messageRecord)
assertThat(
viewModel.uiState.first().uiMessages.first().message,
equalTo("Successfully banned user and deleted all their messages")
equalTo("User banned")
)
}
@ -184,6 +154,8 @@ class ConversationViewModelTest: BaseViewModelTest() {
fun `should remove shown message`() = runBlockingTest {
// Given that a message is generated
whenever(repository.banUser(anyLong(), any())).thenReturn(Result.success(Unit))
whenever(application.getString(any())).thenReturn("User banned")
viewModel.banUser(recipient)
assertThat(viewModel.uiState.value.uiMessages.size, equalTo(1))
// When the message is shown

View File

@ -1,5 +1,6 @@
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.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
@ -23,7 +24,8 @@ interface MessageDataProvider {
fun getMessageIDs(serverIDs: List<Long>, threadID: Long): Pair<List<Long>, List<Long>>
fun deleteMessage(messageID: 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 markMessagesAsDeleted(messages: List<MarkAsDeletedMessage>, isSms: Boolean, displayedMessage: String)
fun getServerHashForMessage(messageID: Long, mms: Boolean): String?
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?

View File

@ -30,6 +30,7 @@ import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
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.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
@ -115,6 +116,7 @@ interface StorageProtocol {
fun persistAttachments(messageID: Long, attachments: List<Attachment>): List<Long>
fun getAttachmentsForMessage(messageID: Long): List<DatabaseAttachment>
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 markAsResyncing(timestamp: Long, author: String)
fun markAsSyncing(timestamp: Long, author: String)
@ -225,6 +227,7 @@ interface StorageProtocol {
fun removeReaction(emoji: String, messageTimestamp: Long, author: String, notifyUnread: Boolean)
fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long)
fun deleteReactions(messageId: Long, mms: Boolean)
fun deleteReactions(messageIds: List<Long>, mms: Boolean)
fun setBlocked(recipients: Iterable<Recipient>, isBlocked: Boolean, fromConfigUpdate: Boolean = false)
fun setRecipientHash(recipient: Recipient, recipientHash: String?)
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

@ -1,7 +1,11 @@
package org.session.libsession.messaging.sending_receiving
import android.text.TextUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import network.loki.messenger.libsession_util.util.ExpiryMode
import org.session.libsession.R
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BackgroundGroupAddJob
@ -41,6 +45,7 @@ import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences
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.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
@ -246,22 +251,65 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest): Long? {
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 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 timestamp = message.timestamp ?: return null
val author = message.author ?: return null
val (messageIdToDelete, mms) = storage.getMessageIdInDatabase(timestamp, author) ?: return null
messageDataProvider.getServerHashForMessage(messageIdToDelete, mms)?.let { serverHash ->
SnodeAPI.deleteMessage(author, listOf(serverHash))
val messageType = storage.getMessageType(timestamp, author) ?: return null
// 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)) {
SSKEnvironment.shared.notificationManager.updateNotification(context)
}
return deletedMessageId
return messageIdToDelete
}
fun handleMessageRequestResponse(message: MessageRequestResponse) {

View File

@ -9,6 +9,7 @@ import com.goterl.lazysodium.interfaces.SecretBox
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import kotlinx.coroutines.coroutineScope
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.bind
@ -18,6 +19,7 @@ import nl.komponents.kovenant.unwrap
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsession.snode.utilities.await
import org.session.libsession.utilities.buildMutableMap
import org.session.libsession.utilities.mapValuesNotNull
import org.session.libsession.utilities.toByteArray
@ -35,6 +37,7 @@ import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.prettifiedDescription
import org.session.libsignal.utilities.retryIfNeeded
import org.session.libsignal.utilities.retryWithUniformInterval
import java.util.Locale
import kotlin.collections.component1
import kotlin.collections.component2
@ -557,50 +560,66 @@ object SnodeAPI {
}
}
fun deleteMessage(publicKey: String, serverHashes: List<String>): Promise<Map<String, Boolean>, Exception> =
retryIfNeeded(maxRetryCount) {
val userED25519KeyPair = getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
val userPublicKey = getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
getSingleTargetSnode(publicKey).bind { snode ->
retryIfNeeded(maxRetryCount) {
val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
val deleteMessageParams = buildMap {
this["pubkey"] = userPublicKey
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
this["messages"] = serverHashes
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
}
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
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
suspend fun deleteMessage(publicKey: String, serverHashes: List<String>) {
retryWithUniformInterval {
val userED25519KeyPair =
getUserED25519KeyPair() ?: throw (Error.NoKeyPair)
val userPublicKey =
getUserPublicKey() ?: throw (Error.NoKeyPair)
val snode = getSingleTargetSnode(publicKey).await()
val verificationData =
sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray()
val deleteMessageParams = buildMap {
this["pubkey"] = userPublicKey
this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString
this["messages"] = serverHashes
this["signature"] = signAndEncode(verificationData, userED25519KeyPair)
}
val rawResponse = invoke(
Snode.Method.DeleteMessage,
snode,
deleteMessageParams,
publicKey
).await()
if (isFailed) {
Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
false
} 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(userPublicKey).plus(serverHashes).plus(hashes).toByteArray()
sodium.cryptoSignVerifyDetached(
Base64.decode(signature),
message,
message.size,
snodePublicKey.asBytes
)
}
}
}
}.fail { e -> Log.e("Loki", "Failed to delete messages", e) }
// thie next step is to verify the nodes on our swarm and check that the message was deleted
// on at least one of them
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: throw (Error.Generic)
val deletedMessages = 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) {
Log.e(
"Loki",
"Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode)."
)
false
} 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(userPublicKey).plus(serverHashes).plus(hashes)
.toByteArray()
sodium.cryptoSignVerifyDetached(
Base64.decode(signature),
message,
message.size,
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
private fun parseSnodes(rawResponse: Any): List<Snode> =

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

View File

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

View File

@ -282,7 +282,6 @@
<item quantity="one">Mesajı sil</item>
<item quantity="other">Mesajları sil</item>
</plurals>
<string name="deleteMessageConfirm">Bu mesajı silmək istədiyinizə əminsiniz?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Mesaj 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="other">Mesajların silinməsi uğursuz oldu</item>
</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="deleteMessagesDescriptionEveryone">Bu mesajları hər kəs üçün silmək istədiyinizə əminsiniz?</string>
<string name="deleting">Silinir</string>

View File

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

View File

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

View File

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

View File

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

View File

@ -281,7 +281,6 @@
<item quantity="one">Suprimeix el missatge</item>
<item quantity="other">Suprimeix els missatges</item>
</plurals>
<string name="deleteMessageConfirm">Esteu segur que voleu suprimir aquest missatge?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Missatge suprimit</item>
<item quantity="other">Missatges suprimits</item>
@ -297,7 +296,6 @@
<item quantity="one">Error en eliminar el missatge</item>
<item quantity="other">Error en eliminar els missatges</item>
</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="deleteMessagesDescriptionEveryone">Esteu segur que voleu suprimir aquests missatges per a tothom?</string>
<string name="deleting">Suprimint</string>

View File

@ -286,7 +286,6 @@
<item quantity="many">Smazat zprávy</item>
<item quantity="other">Smazat zprávy</item>
</plurals>
<string name="deleteMessageConfirm">Opravdu chcete smazat tuto zprávu?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Zpráva smazána</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="other">Nepodařilo se smazat zprávy</item>
</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="deleteMessagesDescriptionEveryone">Jste si jisti, že chcete smazat tyto zprávy pro všechny?</string>
<string name="deleting">Mazání</string>

View File

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

View File

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

View File

@ -282,7 +282,6 @@
<item quantity="one">Nachricht löschen</item>
<item quantity="other">Nachrichten löschen</item>
</plurals>
<string name="deleteMessageConfirm">Möchtest du diese Nachricht wirklich löschen?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Nachricht 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="other">Die Nachrichten konnten nicht gelöscht werden</item>
</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="deleteMessagesDescriptionEveryone">Möchtest du diese Nachrichten wirklich für alle löschen?</string>
<string name="deleting">Wird gelöscht</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -260,7 +260,6 @@
<string name="deleteAfterLegacyDisappearingMessagesLegacy">Ancestral</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="deleteMessageConfirm">Tes a certeza de querer borrar esta mensaxe?</string>
<string name="deleteMessageDeletedGlobally">Esta mensaxe foi eliminada</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>
@ -272,7 +271,6 @@
<item quantity="one">Erro ao borrar a mensaxe</item>
<item quantity="other">Erro ao borrar as mensaxes</item>
</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="deleteMessagesDescriptionEveryone">Tes a certeza de querer eliminar estas mensaxes para todos?</string>
<string name="deleting">Borrando</string>

View File

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

View File

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

View File

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

View File

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

View File

@ -282,7 +282,6 @@
<item quantity="one">Üzenet törlése</item>
<item quantity="other">Üzenetek törlése</item>
</plurals>
<string name="deleteMessageConfirm">Biztos, hogy törölni szeretnéd ezt az üzenetet?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Üzenet 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="other">Nem sikerült az üzenetek törlése</item>
</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="deleteMessagesDescriptionEveryone">Biztos, hogy törölni szeretnéd ezeket az üzeneteket mindenki számára?</string>
<string name="deleting">Törlés</string>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -282,7 +282,6 @@
<item quantity="one">Peyamê Jê Bibe</item>
<item quantity="other">Peyaman Jê Bibe</item>
</plurals>
<string name="deleteMessageConfirm">Tu piştrast î ku tu dixwazî vê peyamê jê bibî?</string>
<plurals name="deleteMessageDeleted">
<item quantity="one">Peyam 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="other">Bi ser neket ku peyaman jê bibe.</item>
</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="deleteMessagesDescriptionEveryone">Tu piştrast î ku tu dixwazî vê peyaman ji kuran jê bike?</string>
<string name="deleting">Jêbibin</string>

View File

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

View File

@ -280,7 +280,6 @@
<plurals name="deleteMessage">
<item quantity="other">메시지 삭제</item>
</plurals>
<string name="deleteMessageConfirm">정말 이 메시지를 삭제하시겠습니까?</string>
<plurals name="deleteMessageDeleted">
<item quantity="other">메시지가 삭제되었습니다.</item>
</plurals>
@ -294,7 +293,6 @@
<plurals name="deleteMessageFailed">
<item quantity="other">메시지를 삭제하지 못했습니다</item>
</plurals>
<string name="deleteMessagesConfirm">정말 이 메시지를 삭제하시겠습니까?</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="deleting">삭제 중…</string>

View File

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

View File

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

View File

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

View File

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

View File

@ -266,7 +266,6 @@
<string name="deleteAfterLegacyDisappearingMessagesLegacy">Mantojums</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="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="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>
@ -279,7 +278,6 @@
<item quantity="one">Neizdevās dzēst ziņu</item>
<item quantity="other">Neizdevās dzēst ziņas</item>
</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="deleteMessagesDescriptionEveryone">Vai esat pārliecināts, ka vēlaties dzēst šos ziņojumus visiem?</string>
<string name="deleting">Dzēšana</string>

View File

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

View File

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

View File

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

View File

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

View File

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

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