mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
Merge remote-tracking branch 'origin/dev' into closed_groups
# Conflicts: # app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt # app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt # app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java # app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt # app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java # app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt # app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt # app/src/main/java/org/thoughtcrime/securesms/notifications/PushReceiver.kt # app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt # app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColors.kt # app/src/main/res/layout/view_control_message.xml # app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt # libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt # libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt # libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt # libsignal/src/main/java/org/session/libsignal/utilities/Retrying.kt
This commit is contained in:
commit
6814f0abe2
@ -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,34 +215,32 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
||||
threadId?.let{ MessagingModuleConfiguration.shared.lastSentTimestampCache.delete(it, messages.map { it.timestamp }) }
|
||||
}
|
||||
|
||||
override fun updateMessageAsDeleted(messageId: Long, isSms: Boolean): Long {
|
||||
val messagingDatabase: MessagingDatabase =
|
||||
if (isSms) DatabaseComponent.get(context).smsDatabase()
|
||||
else DatabaseComponent.get(context).mmsDatabase()
|
||||
|
||||
val isOutgoing = messagingDatabase.isOutgoing(messageId)
|
||||
messagingDatabase.markAsDeleted(messageId)
|
||||
|
||||
if (isOutgoing) {
|
||||
messagingDatabase.deleteMessage(messageId)
|
||||
}
|
||||
|
||||
return messageId
|
||||
}
|
||||
|
||||
override fun updateMessageAsDeleted(timestamp: Long, author: String): Long? {
|
||||
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
|
||||
updateMessageAsDeleted(message.id, !message.isMms)
|
||||
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseComponent.get(context).mmsDatabase()
|
||||
else DatabaseComponent.get(context).smsDatabase()
|
||||
messagingDatabase.markAsDeleted(message.id)
|
||||
if (message.isOutgoing) {
|
||||
messagingDatabase.deleteMessage(message.id)
|
||||
}
|
||||
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
|
||||
@ -120,6 +119,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
|
||||
@ -141,6 +141,7 @@ import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback
|
||||
import org.thoughtcrime.securesms.conversation.v2.menus.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
|
||||
@ -184,8 +185,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
|
||||
@ -257,8 +256,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) {
|
||||
@ -361,9 +358,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 ->
|
||||
@ -424,7 +421,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
// endregion
|
||||
|
||||
fun showOpenUrlDialog(url: String){
|
||||
openLinkDialogUrl = url
|
||||
viewModel.onCommand(ShowOpenUrlDialog(url))
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
@ -437,16 +434,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -456,7 +448,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()
|
||||
}
|
||||
|
||||
@ -675,6 +667,12 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
recyclerScrollState = newState
|
||||
}
|
||||
})
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.isAdmin.collect{
|
||||
adapter.isAdmin = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToMostRecentMessageIfWeShould() {
|
||||
@ -912,6 +910,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
isVisible = state.showInput
|
||||
showMediaControls = state.enableInputMediaControls
|
||||
}
|
||||
|
||||
// show or hide loading indicator
|
||||
binding.loader.isVisible = uiState.showLoader
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1356,7 +1357,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
// `position` is the adapter position; not the visual position
|
||||
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
|
||||
@ -1374,15 +1375,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmojiPicker(message: MessageRecord, visibleMessageView: VisibleMessageView) {
|
||||
private fun showConversationReaction(message: MessageRecord, messageView: View) {
|
||||
val messageContentView = when(messageView){
|
||||
is VisibleMessageView -> messageView.messageContentView
|
||||
is ControlMessageView -> messageView.controlContentView
|
||||
else -> null
|
||||
} ?: return Log.w(TAG, "Failed to show reaction because the messageRecord is not of a known type: $messageView")
|
||||
|
||||
val messageContentBitmap = try {
|
||||
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)
|
||||
@ -1404,14 +1411,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
||||
}
|
||||
|
||||
})
|
||||
val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) }
|
||||
val topLeft = intArrayOf(0, 0).also { messageContentView.getLocationInWindow(it) }
|
||||
val selectedConversationModel = SelectedConversationModel(
|
||||
messageContentBitmap,
|
||||
topLeft[0].toFloat(),
|
||||
topLeft[1].toFloat(),
|
||||
visibleMessageView.messageContentView.width,
|
||||
messageContentView.width,
|
||||
message.isOutgoing,
|
||||
visibleMessageView.messageContentView
|
||||
messageContentView
|
||||
)
|
||||
reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey)
|
||||
}
|
||||
@ -2122,88 +2129,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 || viewModel.isClosedGroupAdmin) && allHasHash) {
|
||||
val bottomSheet = DeleteOptionsBottomSheet()
|
||||
bottomSheet.recipient = recipient
|
||||
bottomSheet.onDeleteForMeTapped = {
|
||||
messages.forEach(viewModel::deleteLocally)
|
||||
bottomSheet.dismiss()
|
||||
endActionMode()
|
||||
}
|
||||
bottomSheet.onDeleteForEveryoneTapped = {
|
||||
messages.forEach(viewModel::deleteForEveryone)
|
||||
bottomSheet.dismiss()
|
||||
endActionMode()
|
||||
}
|
||||
bottomSheet.onCancelTapped = {
|
||||
bottomSheet.dismiss()
|
||||
endActionMode()
|
||||
}
|
||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||
}
|
||||
else // Finally, if this is a closed group and you are deleting someone else's message(s) then we can only delete locally.
|
||||
{
|
||||
showSessionDialog {
|
||||
title(resources.getQuantityString(R.plurals.deleteMessage, messages.count(), messages.count()))
|
||||
text(resources.getString(R.string.deleteMessageDescriptionDevice))
|
||||
dangerButton(R.string.delete) { messages.forEach(viewModel::deleteLocally); endActionMode() }
|
||||
cancelButton(::endActionMode)
|
||||
}
|
||||
}
|
||||
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,7 +1,9 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@ -12,13 +14,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
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 network.loki.messenger.libsession_util.util.GroupMember
|
||||
import org.session.libsession.database.MessageDataProvider
|
||||
@ -29,27 +31,37 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
|
||||
import org.session.libsession.messaging.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.session.libsignal.utilities.AccountId
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.LokiMessageDatabase
|
||||
import org.thoughtcrime.securesms.database.Storage
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.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: StorageProtocol,
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
private val groupDb: GroupDatabase,
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val lokiMessageDb: LokiMessageDatabase,
|
||||
private val textSecurePreferences: TextSecurePreferences
|
||||
) : ViewModel() {
|
||||
|
||||
val showSendAfterApprovalText: Boolean
|
||||
@ -58,8 +70,44 @@ class ConversationViewModel(
|
||||
private val _uiState = MutableStateFlow(ConversationUiState())
|
||||
val uiState: StateFlow<ConversationUiState> get() = _uiState
|
||||
|
||||
private val _dialogsState = MutableStateFlow(DialogsState())
|
||||
val dialogsState: StateFlow<DialogsState> = _dialogsState
|
||||
|
||||
private val _isAdmin = MutableStateFlow(false)
|
||||
val isAdmin: StateFlow<Boolean> = _isAdmin
|
||||
|
||||
private var _recipient: RetrieveOnce<Recipient> = RetrieveOnce {
|
||||
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)
|
||||
@ -274,50 +322,409 @@ 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 && !it.isControlMessage } && (
|
||||
messages.all { it.isOutgoing } ||
|
||||
conversationType == MessageType.COMMUNITY ||
|
||||
messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null
|
||||
})
|
||||
|
||||
// There are three types of dialogs for deletion:
|
||||
// 1- Delete on device only OR all devices - Used for Note to self
|
||||
// 2- Delete on device only OR for everyone - Used for 'admins' or a user's own messages, as long as the message have a server hash
|
||||
// 3- Delete on device only - Used otherwise
|
||||
when {
|
||||
// the conversation is a note to self
|
||||
conversationType == MessageType.NOTE_TO_SELF -> {
|
||||
_dialogsState.update {
|
||||
it.copy(deleteAllDevices = DeleteForEveryoneDialogData(
|
||||
messages = messages,
|
||||
defaultToEveryone = false,
|
||||
everyoneEnabled = true,
|
||||
messageType = conversationType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is an admin or is interacting with their own message And are allowed to delete for everyone
|
||||
(isAdmin.value || allSentByCurrentUser) && canDeleteForEveryone -> {
|
||||
_dialogsState.update {
|
||||
it.copy(
|
||||
deleteEveryone = DeleteForEveryoneDialogData(
|
||||
messages = messages,
|
||||
defaultToEveryone = isAdmin.value,
|
||||
everyoneEnabled = true,
|
||||
messageType = conversationType
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// for non admins, users interacting with someone else's message, or control messages
|
||||
else -> {
|
||||
_dialogsState.update {
|
||||
it.copy(
|
||||
deleteEveryone = DeleteForEveryoneDialogData(
|
||||
messages = messages,
|
||||
defaultToEveryone = false,
|
||||
everyoneEnabled = false, // disable 'delete for everyone' - can only delete locally in this case
|
||||
messageType = conversationType,
|
||||
warning = application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageWarning, messages.count(), messages.count()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This delete the message locally only.
|
||||
* Attachments and other related data will be removed from the db.
|
||||
* If the messages were already marked as deleted they will be removed fully from the db,
|
||||
* otherwise they will appear as a special type of message
|
||||
* that says something like "This message was deleted"
|
||||
*/
|
||||
fun deleteLocally(messages: Set<MessageRecord>) {
|
||||
// make sure to stop audio messages, if any
|
||||
messages.filterIsInstance<MmsMessageRecord>()
|
||||
.mapNotNull { it.slideDeck.audioSlide }
|
||||
.forEach(::stopMessageAudio)
|
||||
|
||||
// if the message was already marked as deleted or control messages, remove it from the db instead
|
||||
if(messages.all { it.isDeleted || it.isControlMessage }){
|
||||
// Remove the message locally (leave nothing behind)
|
||||
repository.deleteMessages(messages = messages, threadId = threadId)
|
||||
} else {
|
||||
// only mark as deleted (message remains behind with "This message was deleted on this device" )
|
||||
repository.markAsDeletedLocally(
|
||||
messages = messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedLocally)
|
||||
)
|
||||
}
|
||||
|
||||
// show confirmation toast
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(R.plurals.deleteMessageDeleted, messages.count(), messages.count()),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
/**
|
||||
* This will mark the messages as deleted, for everyone.
|
||||
* Attachments and other related data will be removed from the db,
|
||||
* but the messages themselves won't be removed from the db.
|
||||
* Instead they will appear as a special type of message
|
||||
* that says something like "This message was deleted"
|
||||
*/
|
||||
private fun markAsDeletedForEveryone(
|
||||
data: DeleteForEveryoneDialogData
|
||||
) = viewModelScope.launch {
|
||||
val recipient = recipient ?: return@launch Log.w("Loki", "Recipient was null for delete for everyone - aborting delete operation.")
|
||||
|
||||
// make sure to stop audio messages, if any
|
||||
data.messages.filterIsInstance<MmsMessageRecord>()
|
||||
.mapNotNull { it.slideDeck.audioSlide }
|
||||
.forEach(::stopMessageAudio)
|
||||
|
||||
// the exact logic for this will depend on the messages type
|
||||
when(data.messageType){
|
||||
MessageType.NOTE_TO_SELF -> markAsDeletedForEveryoneNoteToSelf(data)
|
||||
MessageType.ONE_ON_ONE -> markAsDeletedForEveryone1On1(data)
|
||||
MessageType.LEGACY_GROUP -> markAsDeletedForEveryoneLegacyGroup(data.messages)
|
||||
MessageType.GROUPS_V2 -> markAsDeletedForEveryoneGroupsV2(data)
|
||||
MessageType.COMMUNITY -> markAsDeletedForEveryoneCommunity(data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsDeletedForEveryoneNoteToSelf(data: DeleteForEveryoneDialogData){
|
||||
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// show a loading indicator
|
||||
_uiState.update { it.copy(showLoader = true) }
|
||||
|
||||
// delete remotely
|
||||
try {
|
||||
repository.deleteNoteToSelfMessagesRemotely(threadId, recipient!!, data.messages)
|
||||
|
||||
// When this is done we simply need to remove the message locally (leave nothing behind)
|
||||
repository.deleteMessages(messages = data.messages, threadId = threadId)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageDeleted,
|
||||
data.messages.count(),
|
||||
data.messages.count()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
|
||||
// failed to delete - show a toast and get back on the modal
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageFailed,
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
_dialogsState.update { it.copy(deleteEveryone = data) }
|
||||
}
|
||||
|
||||
// hide loading indicator
|
||||
_uiState.update { it.copy(showLoader = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsDeletedForEveryone1On1(data: DeleteForEveryoneDialogData){
|
||||
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// show a loading indicator
|
||||
_uiState.update { it.copy(showLoader = true) }
|
||||
|
||||
// delete remotely
|
||||
try {
|
||||
repository.delete1on1MessagesRemotely(threadId, recipient!!, data.messages)
|
||||
|
||||
// When this is done we simply need to remove the message locally
|
||||
repository.markAsDeletedLocally(
|
||||
messages = data.messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
|
||||
)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageDeleted,
|
||||
data.messages.count(),
|
||||
data.messages.count()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
|
||||
// failed to delete - show a toast and get back on the modal
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageFailed,
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
_dialogsState.update { it.copy(deleteEveryone = data) }
|
||||
}
|
||||
|
||||
// hide loading indicator
|
||||
_uiState.update { it.copy(showLoader = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsDeletedForEveryoneLegacyGroup(messages: Set<MessageRecord>){
|
||||
if(recipient == null) return showMessage(application.getString(R.string.errorUnknown))
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// delete remotely
|
||||
try {
|
||||
repository.deleteLegacyGroupMessagesRemotely(recipient!!, messages)
|
||||
|
||||
// When this is done we simply need to remove the message locally
|
||||
repository.markAsDeletedLocally(
|
||||
messages = messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
|
||||
)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageDeleted,
|
||||
messages.count(),
|
||||
messages.count()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "FAILED TO delete messages ${messages} ")
|
||||
// failed to delete - show a toast and get back on the modal
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageFailed,
|
||||
messages.size,
|
||||
messages.size
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsDeletedForEveryoneGroupsV2(data: DeleteForEveryoneDialogData){
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// show a loading indicator
|
||||
_uiState.update { it.copy(showLoader = true) }
|
||||
|
||||
//todo GROUPS V2 - uncomment below and use Fanchao's method to delete a group V2
|
||||
try {
|
||||
//repository.callMethodFromFanchao(threadId, recipient, data.messages)
|
||||
|
||||
// the repo will handle the internal logic (calling `/delete` on the swarm
|
||||
// and sending 'GroupUpdateDeleteMemberContentMessage'
|
||||
// When this is done we simply need to remove the message locally
|
||||
repository.markAsDeletedLocally(
|
||||
messages = data.messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
|
||||
)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageDeleted,
|
||||
data.messages.count(), data.messages.count()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
|
||||
// failed to delete - show a toast and get back on the modal
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageFailed,
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
_dialogsState.update { it.copy(deleteAllDevices = data) }
|
||||
}
|
||||
|
||||
// hide loading indicator
|
||||
_uiState.update { it.copy(showLoader = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsDeletedForEveryoneCommunity(data: DeleteForEveryoneDialogData){
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// show a loading indicator
|
||||
_uiState.update { it.copy(showLoader = true) }
|
||||
|
||||
// delete remotely
|
||||
try {
|
||||
repository.deleteCommunityMessagesRemotely(threadId, data.messages)
|
||||
|
||||
// When this is done we simply need to remove the message locally
|
||||
repository.markAsDeletedLocally(
|
||||
messages = data.messages,
|
||||
displayedMessage = application.getString(R.string.deleteMessageDeletedGlobally)
|
||||
)
|
||||
|
||||
// show confirmation toast
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageDeleted,
|
||||
data.messages.count(),
|
||||
data.messages.count()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w("Loki", "FAILED TO delete messages ${data.messages} ")
|
||||
// failed to delete - show a toast and get back on the modal
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
application,
|
||||
application.resources.getQuantityString(
|
||||
R.plurals.deleteMessageFailed,
|
||||
data.messages.size,
|
||||
data.messages.size
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
_dialogsState.update { it.copy(deleteEveryone = data) }
|
||||
}
|
||||
|
||||
// hide loading indicator
|
||||
_uiState.update { it.copy(showLoader = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUserCommunityManager() = openGroup?.let { openGroup ->
|
||||
val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false
|
||||
OpenGroupManager.isUserModerator(application, openGroup.id, userPublicKey, blindedPublicKey)
|
||||
} ?: false
|
||||
|
||||
/**
|
||||
* Stops audio player if its current playing is the one given in the message.
|
||||
*/
|
||||
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()
|
||||
}
|
||||
|
||||
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 setRecipientApproved() {
|
||||
val recipient = recipient ?: return Log.w("Loki", "Recipient was null for set approved action")
|
||||
repository.setApproved(recipient, true)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,13 +733,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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -431,6 +838,40 @@ class ConversationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun onCommand(command: Commands) {
|
||||
when (command) {
|
||||
is Commands.ShowOpenUrlDialog -> {
|
||||
_dialogsState.update {
|
||||
it.copy(openLinkDialogUrl = command.url)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.HideDeleteEveryoneDialog -> {
|
||||
_dialogsState.update {
|
||||
it.copy(deleteEveryone = null)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.HideDeleteAllDevicesDialog -> {
|
||||
_dialogsState.update {
|
||||
it.copy(deleteAllDevices = null)
|
||||
}
|
||||
}
|
||||
|
||||
is Commands.MarkAsDeletedLocally -> {
|
||||
// hide dialog first
|
||||
_dialogsState.update {
|
||||
it.copy(deleteEveryone = null)
|
||||
}
|
||||
|
||||
deleteLocally(command.messages)
|
||||
}
|
||||
is Commands.MarkAsDeletedForEveryone -> {
|
||||
markAsDeletedForEveryone(command.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@dagger.assisted.AssistedFactory
|
||||
interface AssistedFactory {
|
||||
fun create(threadId: Long, edKeyPair: KeyPair?): Factory
|
||||
@ -440,6 +881,7 @@ 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: StorageProtocol,
|
||||
private val messageDataProvider: MessageDataProvider,
|
||||
@ -447,20 +889,48 @@ class ConversationViewModel(
|
||||
private val threadDb: ThreadDatabase,
|
||||
@ApplicationContext
|
||||
private val context: Context,
|
||||
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,
|
||||
groupDb = groupDb,
|
||||
threadDb = threadDb,
|
||||
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)
|
||||
@ -471,6 +941,7 @@ data class ConversationUiState(
|
||||
val shouldExit: Boolean = false,
|
||||
val showInput: Boolean = true,
|
||||
val enableInputMediaControls: Boolean = true,
|
||||
val showLoader: Boolean = false
|
||||
)
|
||||
|
||||
sealed interface MessageRequestUiState {
|
||||
|
@ -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.SodiumUtilities
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.IdPrefix
|
||||
import org.session.libsignal.utilities.AccountId
|
||||
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
|
||||
@ -55,11 +56,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
|
||||
@ -86,7 +89,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 -> {
|
||||
@ -130,7 +133,7 @@ class ControlMessageView : LinearLayout {
|
||||
}
|
||||
|
||||
// remove clicks by default
|
||||
setOnClickListener(null)
|
||||
binding.controlContentView.setOnClickListener(null)
|
||||
hideInfo()
|
||||
|
||||
// handle click behaviour depending on criteria
|
||||
@ -140,7 +143,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,
|
||||
@ -167,7 +170,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,
|
||||
@ -207,6 +210,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)
|
||||
}
|
||||
|
@ -80,6 +80,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
|
||||
|
@ -279,13 +279,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
|
||||
@ -294,16 +293,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:
|
||||
@ -380,7 +380,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),
|
||||
@ -409,7 +409,7 @@ class VisibleMessageView : FrameLayout {
|
||||
)
|
||||
}
|
||||
}
|
||||
message.isSyncing || message.isResyncing ->
|
||||
message.isResyncing ->
|
||||
MessageStatusInfo(
|
||||
R.drawable.ic_delivery_status_sending,
|
||||
context.getColorFromAttr(R.attr.message_status_color),
|
||||
@ -421,16 +421,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
|
||||
}
|
||||
}
|
||||
|
||||
@ -479,10 +484,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)
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
public abstract void markAsDeleted(long messageId);
|
||||
public abstract void markAsDeleted(long messageId, boolean isOutgoing, String displayedMessage);
|
||||
|
||||
public abstract boolean deleteMessage(long messageId);
|
||||
public abstract boolean deleteMessages(long[] messageId, long threadId);
|
||||
|
@ -320,18 +320,21 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
|
||||
db.update(TABLE_NAME, contentValues, ID_WHERE, arrayOf(messageId.toString()))
|
||||
}
|
||||
|
||||
override fun markAsDeleted(messageId: Long) {
|
||||
override fun markAsDeleted(messageId: Long, isOutgoing: Boolean, displayedMessage: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val 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 { 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) {
|
||||
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
|
||||
|
@ -65,6 +65,9 @@ import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.getClosedGroup
|
||||
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
|
||||
import org.session.libsignal.messages.SignalServiceAttachmentPointer
|
||||
@ -658,6 +661,12 @@ open class Storage @Inject constructor(
|
||||
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,
|
||||
@ -1807,6 +1816,12 @@ open class Storage @Inject constructor(
|
||||
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 = recipientDatabase
|
||||
recipientDb.setBlocked(recipients, isBlocked)
|
||||
|
@ -543,7 +543,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
} else {
|
||||
showMuteDialog(this) { until ->
|
||||
lifecycleScope.launch(Dispatchers.Default) {
|
||||
Log.d("", "**** until: $until")
|
||||
recipientDatabase.setMuted(thread.recipient, until)
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
||||
|
@ -1,7 +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
|
||||
@ -38,39 +40,52 @@ class PushReceiver @Inject constructor(
|
||||
) {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
fun onPush(dataMap: Map<String, String>?) {
|
||||
val result = dataMap?.decodeAndDecrypt()
|
||||
val data = result?.first
|
||||
if (data == null) {
|
||||
onPush()
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
handlePushData(data = data, metadata = result.second)
|
||||
}
|
||||
|
||||
private fun handlePushData(data: ByteArray, metadata: PushNotificationMetadata?) {
|
||||
try {
|
||||
val params = when {
|
||||
metadata?.namespace == Namespace.CLOSED_GROUP_MESSAGES() -> {
|
||||
val groupId = AccountId(requireNotNull(metadata.account) {
|
||||
pushData.metadata?.namespace == Namespace.CLOSED_GROUP_MESSAGES() -> {
|
||||
val groupId = AccountId(requireNotNull(pushData.metadata.account) {
|
||||
"Received a closed group message push notification without an account ID"
|
||||
})
|
||||
|
||||
val envelop = checkNotNull(tryDecryptGroupMessage(groupId, data)) {
|
||||
val envelop = checkNotNull(tryDecryptGroupMessage(groupId, pushData.data)) {
|
||||
"Unable to decrypt closed group message"
|
||||
}
|
||||
|
||||
MessageReceiveParameters(
|
||||
data = envelop.toByteArray(),
|
||||
serverHash = metadata.msg_hash,
|
||||
serverHash = pushData.metadata.msg_hash,
|
||||
closedGroup = Destination.ClosedGroup(groupId.hexString)
|
||||
)
|
||||
}
|
||||
|
||||
metadata?.namespace == 0 || metadata == null -> {
|
||||
pushData.metadata?.namespace == 0 || pushData.metadata == null -> {
|
||||
val envelopeAsData = MessageWrapper.unwrap(pushData.data).toByteArray()
|
||||
MessageReceiveParameters(
|
||||
data = MessageWrapper.unwrap(data).toByteArray(),
|
||||
data = envelopeAsData,
|
||||
serverHash = pushData.metadata?.msg_hash
|
||||
)
|
||||
}
|
||||
|
||||
@ -84,7 +99,9 @@ class PushReceiver @Inject constructor(
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to unwrap data for message due to error.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun tryDecryptGroupMessage(groupId: AccountId, data: ByteArray): Envelope? {
|
||||
val (envelopBytes, sender) = checkNotNull(configFactory.withGroupConfigs(groupId) { it.groupKeys.decrypt(data) }) {
|
||||
@ -98,8 +115,18 @@ class PushReceiver @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
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))
|
||||
@ -111,12 +138,10 @@ class PushReceiver @Inject constructor(
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
|
||||
if (context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
|
||||
NotificationManagerCompat.from(context).notify(11111, builder.build())
|
||||
}
|
||||
NotificationManagerCompat.from(context).notify(11111, builder.build())
|
||||
}
|
||||
|
||||
private fun Map<String, String>.decodeAndDecrypt() =
|
||||
private fun Map<String, String>.asPushData(): PushData =
|
||||
when {
|
||||
// this is a v2 push notification
|
||||
containsKey("spns") -> {
|
||||
@ -124,14 +149,14 @@ class PushReceiver @Inject constructor(
|
||||
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(it) to null }
|
||||
else -> PushData(this["ENCRYPTED_DATA"]?.let(Base64::decode), null)
|
||||
}
|
||||
|
||||
private fun decrypt(encPayload: ByteArray): Pair<ByteArray?, PushNotificationMetadata?> {
|
||||
private fun decrypt(encPayload: ByteArray): PushData {
|
||||
Log.d(TAG, "decrypt() called")
|
||||
|
||||
val encKey = getOrCreateNotificationKey()
|
||||
@ -149,11 +174,14 @@ class PushReceiver @Inject constructor(
|
||||
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" }
|
||||
} to metadata
|
||||
}
|
||||
}
|
||||
|
||||
fun getOrCreateNotificationKey(): Key {
|
||||
@ -167,4 +195,9 @@ class PushReceiver @Inject constructor(
|
||||
IdentityKeyUtil.save(context, IdentityKeyUtil.NOTIFICATION_KEY, key.asHexString)
|
||||
return key
|
||||
}
|
||||
|
||||
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 ->
|
||||
|
@ -14,6 +14,7 @@ import org.session.libsession.database.MessageDataProvider
|
||||
import org.session.libsession.database.userAuth
|
||||
import org.session.libsession.messaging.groups.GroupManagerV2
|
||||
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
|
||||
@ -45,6 +46,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface ConversationRepository {
|
||||
fun maybeGetRecipientForThreadId(threadId: Long): Recipient?
|
||||
@ -56,13 +59,29 @@ interface ConversationRepository {
|
||||
fun clearDrafts(threadId: Long)
|
||||
fun inviteContacts(threadId: Long, contacts: List<Recipient>)
|
||||
fun setBlocked(threadId: Long, recipient: Recipient, blocked: Boolean)
|
||||
fun deleteLocally(recipient: Recipient, message: MessageRecord)
|
||||
fun markAsDeletedLocally(messages: Set<MessageRecord>, displayedMessage: String)
|
||||
fun deleteMessages(messages: Set<MessageRecord>, threadId: Long)
|
||||
fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord)
|
||||
fun setApproved(recipient: Recipient, isApproved: Boolean)
|
||||
fun isKicked(recipient: Recipient): Boolean
|
||||
|
||||
suspend fun deleteForEveryone(threadId: Long, recipient: Recipient, message: MessageRecord): Result<Unit>
|
||||
suspend fun deleteMessageWithoutUnsendRequest(threadId: Long, messages: Set<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 banUser(threadId: Long, recipient: Recipient): Result<Unit>
|
||||
suspend fun banAndDeleteAll(threadId: Long, recipient: Recipient): Result<Unit>
|
||||
suspend fun deleteThread(threadId: Long): Result<Unit>
|
||||
@ -174,14 +193,57 @@ class DefaultConversationRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteLocally(recipient: Recipient, message: MessageRecord) {
|
||||
if (shouldSendUnsendRequest(recipient)) {
|
||||
textSecurePreferences.getLocalNumber()?.let {
|
||||
MessageSender.send(buildUnsendRequest(message), 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)
|
||||
}
|
||||
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
if(sms.isNotEmpty()){
|
||||
messageDataProvider.deleteMessages(sms.map { it.id }, threadId, isSms = true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will mark the messages as deleted.
|
||||
* They won't be removed from the db but instead will appear as a special type
|
||||
* of message that says something like "This message was deleted"
|
||||
*/
|
||||
override fun markAsDeletedLocally(messages: Set<MessageRecord>, displayedMessage: String) {
|
||||
// split the messages into mms and sms
|
||||
val (mms, sms) = messages.partition { it.isMms }
|
||||
|
||||
if(mms.isNotEmpty()){
|
||||
messageDataProvider.markMessagesAsDeleted(mms.map { MarkAsDeletedMessage(
|
||||
messageId = it.id,
|
||||
isOutgoing = it.isOutgoing
|
||||
) },
|
||||
isSms = false,
|
||||
displayedMessage = displayedMessage
|
||||
)
|
||||
|
||||
// delete reactions
|
||||
storage.deleteReactions(messageIds = mms.map { it.id }, mms = true)
|
||||
}
|
||||
|
||||
if(sms.isNotEmpty()){
|
||||
messageDataProvider.markMessagesAsDeleted(sms.map { MarkAsDeletedMessage(
|
||||
messageId = it.id,
|
||||
isOutgoing = it.isOutgoing
|
||||
) },
|
||||
isSms = true,
|
||||
displayedMessage = displayedMessage
|
||||
)
|
||||
|
||||
// delete reactions
|
||||
storage.deleteReactions(messageIds = sms.map { it.id }, mms = false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAllLocalMessagesInThreadFromSenderOfMessage(messageRecord: MessageRecord) {
|
||||
@ -197,74 +259,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> {
|
||||
return runCatching {
|
||||
withContext(Dispatchers.Default) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
val serverId = lokiMessageDb.getServerID(message.id, !message.isMms)
|
||||
if (serverId != null) {
|
||||
OpenGroupApi.deleteMessage(
|
||||
serverID = serverId,
|
||||
room = openGroup.room,
|
||||
server = openGroup.server
|
||||
).await()
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
} else {
|
||||
// If the server ID is null then this message is stuck in limbo (it has likely been
|
||||
// deleted remotely but that deletion did not occur locally) - so we'll delete the
|
||||
// message locally to clean up.
|
||||
Log.w(
|
||||
"ConversationRepository",
|
||||
"Found community message without a server ID - deleting locally."
|
||||
)
|
||||
messages: Set<MessageRecord>
|
||||
) {
|
||||
// delete the messages remotely
|
||||
val publicKey = recipient.address.serialize()
|
||||
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
} else // If this thread is NOT in a Community
|
||||
{
|
||||
val serverHash =
|
||||
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
|
||||
if (serverHash != null) {
|
||||
var publicKey = recipient.address.serialize()
|
||||
if (recipient.isLegacyClosedGroupRecipient) {
|
||||
publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString()
|
||||
}
|
||||
|
||||
if (recipient.isClosedGroupV2Recipient) {
|
||||
// admin check internally, assume either admin or all belong to user
|
||||
groupManager.requestMessageDeletion(
|
||||
groupId = AccountId(publicKey),
|
||||
messageHashes = listOf(serverHash)
|
||||
)
|
||||
} else {
|
||||
SnodeAPI.deleteMessage(
|
||||
publicKey = publicKey,
|
||||
swarmAuth = storage.userAuth!!,
|
||||
serverHashes = listOf(serverHash)
|
||||
).await()
|
||||
}
|
||||
|
||||
if (shouldSendUnsendRequest(recipient)) {
|
||||
MessageSender.send(
|
||||
message = buildUnsendRequest(message),
|
||||
address = recipient.address,
|
||||
)
|
||||
}
|
||||
}
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
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) }
|
||||
}
|
||||
|
||||
// send an UnsendRequest to recipient's swarm
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
MessageSender.send(unsendRequest, recipient.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteLegacyGroupMessagesRemotely(
|
||||
recipient: Recipient,
|
||||
messages: Set<MessageRecord>
|
||||
) {
|
||||
if (recipient.isClosedGroupRecipient) {
|
||||
val publicKey = recipient.address
|
||||
|
||||
messages.forEach { message ->
|
||||
// send an UnsendRequest to group's swarm
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
MessageSender.send(unsendRequest, publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteNoteToSelfMessagesRemotely(
|
||||
threadId: Long,
|
||||
recipient: Recipient,
|
||||
messages: Set<MessageRecord>
|
||||
) {
|
||||
// delete the messages remotely
|
||||
val publicKey = recipient.address.serialize()
|
||||
val userAddress: Address? = textSecurePreferences.getLocalNumber()?.let { Address.fromSerialized(it) }
|
||||
|
||||
messages.forEach { message ->
|
||||
// delete from swarm
|
||||
messageDataProvider.getServerHashForMessage(message.id, message.isMms)
|
||||
?.let { serverHash ->
|
||||
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
|
||||
}
|
||||
|
||||
// send an UnsendRequest to user's swarm
|
||||
buildUnsendRequest(recipient, message)?.let { unsendRequest ->
|
||||
userAddress?.let { MessageSender.send(unsendRequest, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -280,35 +350,6 @@ class DefaultConversationRepository @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteMessageWithoutUnsendRequest(
|
||||
threadId: Long,
|
||||
messages: Set<MessageRecord>
|
||||
): Result<Unit> = kotlin.runCatching {
|
||||
withContext(Dispatchers.Default) {
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
|
||||
if (openGroup != null) {
|
||||
val messageServerIDs = mutableMapOf<Long, MessageRecord>()
|
||||
for (message in messages) {
|
||||
val messageServerID =
|
||||
lokiMessageDb.getServerID(message.id, !message.isMms) ?: continue
|
||||
messageServerIDs[messageServerID] = message
|
||||
}
|
||||
messageServerIDs.forEach { (messageServerID, message) ->
|
||||
OpenGroupApi.deleteMessage(messageServerID, openGroup.room, openGroup.server).await()
|
||||
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||
}
|
||||
} else {
|
||||
for (message in messages) {
|
||||
if (message.isMms) {
|
||||
mmsDb.deleteMessage(message.id)
|
||||
} else {
|
||||
smsDb.deleteMessage(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun banUser(threadId: Long, recipient: Recipient): Result<Unit> = runCatching {
|
||||
val accountID = recipient.address.toString()
|
||||
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
|
||||
|
@ -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
|
||||
|
@ -21,6 +21,7 @@ interface ThemeColors {
|
||||
val warning: Color
|
||||
val textAlert: Color
|
||||
val danger: Color
|
||||
val warning: Color
|
||||
val disabled: Color
|
||||
val background: Color
|
||||
val backgroundSecondary: Color
|
||||
@ -102,7 +103,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 = warningUniversal
|
||||
override val warning = primaryOrange
|
||||
override val disabled = disabledDark
|
||||
override val background = classicDark0
|
||||
override val backgroundSecondary = classicDark1
|
||||
@ -122,7 +123,7 @@ data class ClassicDark(override val primary: Color = primaryGreen) : ThemeColors
|
||||
data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColors {
|
||||
override val isLight = true
|
||||
override val danger = dangerLight
|
||||
override val warning = warningUniversal
|
||||
override val warning = primaryOrange
|
||||
override val disabled = disabledLight
|
||||
override val background = classicLight6
|
||||
override val backgroundSecondary = classicLight5
|
||||
@ -142,7 +143,7 @@ data class ClassicLight(override val primary: Color = primaryGreen) : ThemeColor
|
||||
data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
|
||||
override val isLight = false
|
||||
override val danger = dangerDark
|
||||
override val warning = warningUniversal
|
||||
override val warning = primaryOrange
|
||||
override val disabled = disabledDark
|
||||
override val background = oceanDark2
|
||||
override val backgroundSecondary = oceanDark1
|
||||
@ -162,7 +163,7 @@ data class OceanDark(override val primary: Color = primaryBlue) : ThemeColors {
|
||||
data class OceanLight(override val primary: Color = primaryBlue) : ThemeColors {
|
||||
override val isLight = true
|
||||
override val danger = dangerLight
|
||||
override val warning = warningUniversal
|
||||
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>
|
||||
|
||||
|
@ -386,4 +386,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"
|
||||
android:layout_width="wrap_content"
|
||||
<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_pending_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"/>
|
||||
|
@ -164,7 +164,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,6 +23,6 @@ class FirebasePushService : FirebaseMessagingService() {
|
||||
|
||||
override fun onMessageReceived(message: RemoteMessage) {
|
||||
Log.d(TAG, "Received a push notification.")
|
||||
pushReceiver.onPush(message.data)
|
||||
pushReceiver.onPushDataReceived(message.data)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -27,6 +27,7 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
|
||||
private val repository = mock<ConversationRepository>()
|
||||
private val storage = mock<Storage>()
|
||||
private val application = mock<Application>()
|
||||
|
||||
private val threadId = 123L
|
||||
private val edKeyPair = mock<KeyPair>()
|
||||
@ -41,7 +42,10 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
storage = storage,
|
||||
messageDataProvider = mock(),
|
||||
groupDb = mock(),
|
||||
threadDb = mock()
|
||||
threadDb = mock(),
|
||||
textSecurePreferences = mock(),
|
||||
lokiMessageDb = mock(),
|
||||
application = mock(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -94,59 +98,27 @@ class ConversationViewModelTest: BaseViewModelTest() {
|
||||
verify(repository).setBlocked(threadId, recipient, false)
|
||||
}
|
||||
|
||||
@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")
|
||||
)
|
||||
}
|
||||
|
||||
@ -154,21 +126,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")
|
||||
)
|
||||
}
|
||||
|
||||
@ -176,6 +150,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,8 +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 updateMessageAsDeleted(messageId: Long, isSms: Boolean): 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?
|
||||
|
@ -36,6 +36,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
|
||||
@ -124,6 +125,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)
|
||||
@ -254,6 +256,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
|
||||
)
|
@ -6,6 +6,7 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||
import network.loki.messenger.libsession_util.util.Sodium
|
||||
import org.session.libsession.R
|
||||
import org.session.libsession.avatars.AvatarHelper
|
||||
import org.session.libsession.database.userAuth
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
@ -50,6 +51,7 @@ import org.session.libsession.utilities.ProfileKeyUtil
|
||||
import org.session.libsession.utilities.SSKEnvironment
|
||||
import org.session.libsession.utilities.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
|
||||
@ -258,23 +260,66 @@ 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 userAuth = storage.userAuth ?: return null
|
||||
val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key ->
|
||||
var admin = false
|
||||
val groupID = doubleEncodeGroupID(key)
|
||||
val group = storage.getGroup(groupID)
|
||||
if(group != null) {
|
||||
admin = group.admins.map { it.toString() }.contains(message.sender)
|
||||
}
|
||||
admin
|
||||
} ?: false
|
||||
|
||||
// First we need to determine the validity of the UnsendRequest
|
||||
// It is valid if:
|
||||
val requestIsValid = message.sender == message.author || // the sender is the author of the message
|
||||
message.author == userPublicKey || // the sender is the current user
|
||||
isLegacyGroupAdmin // sender is an admin of legacy group
|
||||
|
||||
if (!requestIsValid) { return null }
|
||||
|
||||
val context = MessagingModuleConfiguration.shared.context
|
||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||
val 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, swarmAuth = userAuth, 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) {
|
||||
|
@ -18,6 +18,8 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
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
|
||||
@ -867,63 +869,70 @@ object SnodeAPI {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun deleteMessage(
|
||||
publicKey: String,
|
||||
swarmAuth: SwarmAuth,
|
||||
serverHashes: List<String>
|
||||
): Promise<Map<String, Boolean>, Exception> = scope.retrySuspendAsPromise(maxRetryCount) {
|
||||
val params = buildAuthenticatedParameters(
|
||||
auth = swarmAuth,
|
||||
namespace = null,
|
||||
verificationData = { _, _ ->
|
||||
buildString {
|
||||
append(Snode.Method.DeleteMessage.rawValue)
|
||||
serverHashes.forEach(this::append)
|
||||
|
||||
suspend fun deleteMessage(publicKey: String, swarmAuth: SwarmAuth, serverHashes: List<String>) {
|
||||
retryWithUniformInterval {
|
||||
val snode = getSingleTargetSnode(publicKey).await()
|
||||
val params = buildAuthenticatedParameters(
|
||||
auth = swarmAuth,
|
||||
namespace = null,
|
||||
verificationData = { _, _ ->
|
||||
buildString {
|
||||
append(Snode.Method.DeleteMessage.rawValue)
|
||||
serverHashes.forEach(this::append)
|
||||
}
|
||||
}
|
||||
) {
|
||||
this["messages"] = serverHashes
|
||||
}
|
||||
) {
|
||||
this["messages"] = serverHashes
|
||||
}
|
||||
val rawResponse = invoke(
|
||||
Snode.Method.DeleteMessage,
|
||||
snode,
|
||||
params,
|
||||
publicKey
|
||||
).await()
|
||||
|
||||
val snode = getSingleTargetSnode(publicKey).await()
|
||||
val rawResponse = invoke(Snode.Method.DeleteMessage, snode, params, publicKey).await()
|
||||
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@retrySuspendAsPromise 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
|
||||
// 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)
|
||||
|
||||
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(swarmAuth.accountId.hexString)
|
||||
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(swarmAuth.accountId.hexString)
|
||||
.plus(serverHashes)
|
||||
.plus(hashes)
|
||||
.toByteArray()
|
||||
sodium.cryptoSignVerifyDetached(
|
||||
Base64.decode(signature),
|
||||
message,
|
||||
message.size,
|
||||
snodePublicKey.asBytes
|
||||
)
|
||||
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> =
|
||||
(rawResponse as? Map<*, *>)
|
||||
|
@ -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…
Reference in New Issue
Block a user