mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 01:07:47 +00:00
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:
parent
ecfa5d346a
commit
54ef260aa9
@ -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"
|
||||
|
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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? =
|
||||
|
@ -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 {
|
@ -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 {
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
|
||||
|
||||
/**
|
||||
* Represents an action to be rendered
|
||||
*/
|
||||
|
@ -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>) {
|
||||
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
@ -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?) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(){
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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 = ?"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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?
|
||||
)
|
||||
}
|
||||
|
@ -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 ->
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,6 @@
|
||||
package org.session.libsession.messaging.messages
|
||||
|
||||
data class MarkAsDeletedMessage(
|
||||
val messageId: Long,
|
||||
val isOutgoing: Boolean
|
||||
)
|
@ -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) {
|
||||
|
@ -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> =
|
||||
|
@ -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
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user