mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-25 01:07:47 +00:00
commit
39d997a636
@ -19,6 +19,7 @@ import org.session.libsignal.utilities.guava.Optional
|
|||||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||||
import org.thoughtcrime.securesms.database.Database
|
import org.thoughtcrime.securesms.database.Database
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
|
import org.thoughtcrime.securesms.database.MessagingDatabase
|
||||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||||
import org.thoughtcrime.securesms.events.PartProgressEvent
|
import org.thoughtcrime.securesms.events.PartProgressEvent
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
@ -167,14 +168,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
override fun deleteMessage(messageID: Long, isSms: Boolean) {
|
||||||
if (isSms) {
|
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseFactory.getSmsDatabase(context)
|
||||||
val db = DatabaseFactory.getSmsDatabase(context)
|
else DatabaseFactory.getMmsDatabase(context)
|
||||||
db.deleteMessage(messageID)
|
messagingDatabase.deleteMessage(messageID)
|
||||||
} else {
|
|
||||||
val db = DatabaseFactory.getMmsDatabase(context)
|
|
||||||
db.delete(messageID)
|
|
||||||
}
|
|
||||||
DatabaseFactory.getLokiMessageDatabase(context).deleteMessage(messageID, isSms)
|
DatabaseFactory.getLokiMessageDatabase(context).deleteMessage(messageID, isSms)
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).deleteMessageServerHash(messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateMessageAsDeleted(timestamp: Long, author: String) {
|
||||||
|
val database = DatabaseFactory.getMmsSmsDatabase(context)
|
||||||
|
val address = Address.fromSerialized(author)
|
||||||
|
val message = database.getMessageFor(timestamp, address) ?: return
|
||||||
|
val messagingDatabase: MessagingDatabase = if (message.isMms) DatabaseFactory.getMmsDatabase(context)
|
||||||
|
else DatabaseFactory.getSmsDatabase(context)
|
||||||
|
messagingDatabase.markAsDeleted(message.id, message.isRead)
|
||||||
|
if (message.isOutgoing) {
|
||||||
|
messagingDatabase.deleteMessage(message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getServerHashForMessage(messageID: Long): String? {
|
||||||
|
val messageDB = DatabaseFactory.getLokiMessageDatabase(context)
|
||||||
|
return messageDB.getMessageServerHash(messageID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
override fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment? {
|
||||||
|
@ -50,6 +50,7 @@ import org.session.libsession.messaging.contacts.Contact
|
|||||||
import org.session.libsession.messaging.mentions.Mention
|
import org.session.libsession.messaging.mentions.Mention
|
||||||
import org.session.libsession.messaging.mentions.MentionsManager
|
import org.session.libsession.messaging.mentions.MentionsManager
|
||||||
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
import org.session.libsession.messaging.messages.control.DataExtractionNotification
|
||||||
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
|
||||||
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
|
||||||
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
|
||||||
@ -59,8 +60,10 @@ import org.session.libsession.messaging.sending_receiving.MessageSender
|
|||||||
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
|
||||||
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
import org.session.libsession.utilities.Address.Companion.fromSerialized
|
||||||
|
import org.session.libsession.utilities.GroupUtil
|
||||||
import org.session.libsession.utilities.MediaTypes
|
import org.session.libsession.utilities.MediaTypes
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsession.utilities.concurrent.SimpleTask
|
import org.session.libsession.utilities.concurrent.SimpleTask
|
||||||
@ -70,6 +73,7 @@ import org.session.libsignal.crypto.MnemonicCodec
|
|||||||
import org.session.libsignal.utilities.ListenableFuture
|
import org.session.libsignal.utilities.ListenableFuture
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
import org.session.libsignal.utilities.hexEncodedPrivateKey
|
||||||
|
import org.session.libsignal.utilities.toHexString
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.audio.AudioRecorder
|
import org.thoughtcrime.securesms.audio.AudioRecorder
|
||||||
@ -205,6 +209,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
const val PICK_GIF = 10
|
const val PICK_GIF = 10
|
||||||
const val PICK_FROM_LIBRARY = 12
|
const val PICK_FROM_LIBRARY = 12
|
||||||
const val INVITE_CONTACTS = 124
|
const val INVITE_CONTACTS = 124
|
||||||
|
|
||||||
|
//flag
|
||||||
|
const val IS_UNSEND_REQUESTS_ENABLED = false
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@ -1114,7 +1121,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
|
stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteMessages(messages: Set<MessageRecord>) {
|
private fun buildUnsendRequest(message: MessageRecord): UnsendRequest? {
|
||||||
|
if (this.thread.isOpenGroupRecipient) return null
|
||||||
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
|
messageDataProvider.getServerHashForMessage(message.id) ?: return null
|
||||||
|
val unsendRequest = UnsendRequest()
|
||||||
|
if (message.isOutgoing) {
|
||||||
|
unsendRequest.author = TextSecurePreferences.getLocalNumber(this)
|
||||||
|
} else {
|
||||||
|
unsendRequest.author = message.individualRecipient.address.contactIdentifier()
|
||||||
|
}
|
||||||
|
unsendRequest.timestamp = message.timestamp
|
||||||
|
|
||||||
|
return unsendRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteLocally(message: MessageRecord) {
|
||||||
|
buildUnsendRequest(message)?.let { unsendRequest ->
|
||||||
|
TextSecurePreferences.getLocalNumber(this@ConversationActivityV2)?.let {
|
||||||
|
MessageSender.send(unsendRequest, Address.fromSerialized(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteForEveryone(message: MessageRecord) {
|
||||||
|
buildUnsendRequest(message)?.let { unsendRequest ->
|
||||||
|
MessageSender.send(unsendRequest, thread.address)
|
||||||
|
}
|
||||||
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
|
val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2)
|
||||||
|
val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)
|
||||||
|
if (openGroup != null) {
|
||||||
|
messageDB.getServerID(message.id, !message.isMms)?.let { messageServerID ->
|
||||||
|
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server)
|
||||||
|
.success {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
}.failUi { error ->
|
||||||
|
Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messageDataProvider.deleteMessage(message.id, !message.isMms)
|
||||||
|
messageDataProvider.getServerHashForMessage(message.id)?.let { serverHash ->
|
||||||
|
var publicKey = thread.address.serialize()
|
||||||
|
if (thread.isClosedGroupRecipient) { publicKey = GroupUtil.doubleDecodeGroupID(publicKey).toHexString() }
|
||||||
|
SnodeAPI.deleteMessage(publicKey, listOf(serverHash))
|
||||||
|
.failUi { error ->
|
||||||
|
Toast.makeText(this@ConversationActivityV2, "Couldn't delete message due to error: $error", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove this after the unsend request is enabled
|
||||||
|
fun deleteMessagesWithoutUnsendRequest(messages: Set<MessageRecord>) {
|
||||||
val messageCount = messages.size
|
val messageCount = messages.size
|
||||||
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2)
|
val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2)
|
||||||
@ -1141,7 +1202,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
} else {
|
} else {
|
||||||
for (message in messages) {
|
for (message in messages) {
|
||||||
if (message.isMms) {
|
if (message.isMms) {
|
||||||
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id)
|
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
|
||||||
} else {
|
} else {
|
||||||
DatabaseFactory.getSmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
|
DatabaseFactory.getSmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
|
||||||
}
|
}
|
||||||
@ -1156,6 +1217,72 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
builder.show()
|
builder.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteMessages(messages: Set<MessageRecord>) {
|
||||||
|
if (!IS_UNSEND_REQUESTS_ENABLED) {
|
||||||
|
deleteMessagesWithoutUnsendRequest(messages)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val allSentByCurrentUser = messages.all { it.isOutgoing }
|
||||||
|
val allHasHash = messages.all { DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2).getMessageServerHash(it.id) != null }
|
||||||
|
if (thread.isOpenGroupRecipient) {
|
||||||
|
val messageCount = messages.size
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
|
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
|
builder.setCancelable(true)
|
||||||
|
builder.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
|
for (message in messages) {
|
||||||
|
this.deleteForEveryone(message)
|
||||||
|
}
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
builder.show()
|
||||||
|
} else if (allSentByCurrentUser && allHasHash) {
|
||||||
|
val bottomSheet = DeleteOptionsBottomSheet()
|
||||||
|
bottomSheet.recipient = thread
|
||||||
|
bottomSheet.onDeleteForMeTapped = {
|
||||||
|
for (message in messages) {
|
||||||
|
this.deleteLocally(message)
|
||||||
|
}
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.onDeleteForEveryoneTapped = {
|
||||||
|
for (message in messages) {
|
||||||
|
this.deleteForEveryone(message)
|
||||||
|
}
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.onCancelTapped = {
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||||
|
} else {
|
||||||
|
val messageCount = messages.size
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount))
|
||||||
|
builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount))
|
||||||
|
builder.setCancelable(true)
|
||||||
|
builder.setPositiveButton(R.string.delete) { _, _ ->
|
||||||
|
for (message in messages) {
|
||||||
|
this.deleteLocally(message)
|
||||||
|
}
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
endActionMode()
|
||||||
|
}
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun banUser(messages: Set<MessageRecord>) {
|
override fun banUser(messages: Set<MessageRecord>) {
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
val sessionID = messages.first().individualRecipient.address.toString()
|
val sessionID = messages.first().individualRecipient.address.toString()
|
||||||
|
@ -73,9 +73,11 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
|
|||||||
val position = viewHolder.adapterPosition
|
val position = viewHolder.adapterPosition
|
||||||
view.indexInAdapter = position
|
view.indexInAdapter = position
|
||||||
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
|
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
|
||||||
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
if (!message.isDeleted) {
|
||||||
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
|
||||||
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
|
||||||
|
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
|
||||||
|
}
|
||||||
view.contentViewDelegate = visibleMessageContentViewDelegate
|
view.contentViewDelegate = visibleMessageContentViewDelegate
|
||||||
}
|
}
|
||||||
is ControlMessageViewHolder -> viewHolder.view.bind(message)
|
is ControlMessageViewHolder -> viewHolder.view.bind(message)
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
|
||||||
|
import kotlinx.android.synthetic.main.fragment_delete_message_bottom_sheet.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
|
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||||
|
|
||||||
|
class DeleteOptionsBottomSheet: BottomSheetDialogFragment(), View.OnClickListener {
|
||||||
|
|
||||||
|
lateinit var recipient: Recipient
|
||||||
|
|
||||||
|
var onDeleteForMeTapped: (() -> Unit?)? = null
|
||||||
|
var onDeleteForEveryoneTapped: (() -> Unit)? = null
|
||||||
|
var onCancelTapped: (() -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_delete_message_bottom_sheet, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClick(v: View?) {
|
||||||
|
when (v) {
|
||||||
|
deleteForMeTextView -> onDeleteForMeTapped?.invoke()
|
||||||
|
deleteForEveryoneTextView -> onDeleteForEveryoneTapped?.invoke()
|
||||||
|
cancelTextView -> onCancelTapped?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
if (!this::recipient.isInitialized) { return dismiss() }
|
||||||
|
if (!recipient.isGroupRecipient) {
|
||||||
|
deleteForEveryoneTextView.text = resources.getString(R.string.delete_message_for_me_and_recipient, recipient.name)
|
||||||
|
}
|
||||||
|
deleteForMeTextView.setOnClickListener(this)
|
||||||
|
deleteForEveryoneTextView.setOnClickListener(this)
|
||||||
|
cancelTextView.setOnClickListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
val window = dialog?.window ?: return
|
||||||
|
val isLightMode = UiModeUtilities.isDayUiMode(requireContext())
|
||||||
|
window.setDimAmount(if (isLightMode) 0.1f else 0.75f)
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import android.view.MenuItem
|
|||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
|
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||||
@ -34,8 +35,17 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
|
|||||||
val thread = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!
|
val thread = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadID)!!
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
fun userCanDeleteSelectedItems(): Boolean {
|
fun userCanDeleteSelectedItems(): Boolean {
|
||||||
if (openGroup == null) { return true }
|
|
||||||
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
|
||||||
|
|
||||||
|
// Remove this after the unsend request is enabled
|
||||||
|
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
|
||||||
|
if (openGroup == null) { return true }
|
||||||
|
if (allSentByCurrentUser) { return true }
|
||||||
|
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
|
||||||
|
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
|
||||||
if (allSentByCurrentUser) { return true }
|
if (allSentByCurrentUser) { return true }
|
||||||
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
package org.thoughtcrime.securesms.conversation.v2.messages
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.view.*
|
||||||
|
import kotlinx.android.synthetic.main.view_deleted_message.view.*
|
||||||
|
import kotlinx.android.synthetic.main.view_document.view.*
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||||
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class DeletedMessageView : LinearLayout {
|
||||||
|
|
||||||
|
// region Lifecycle
|
||||||
|
constructor(context: Context) : super(context) { initialize() }
|
||||||
|
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize() }
|
||||||
|
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize() }
|
||||||
|
|
||||||
|
private fun initialize() {
|
||||||
|
LayoutInflater.from(context).inflate(R.layout.view_deleted_message, this)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Updating
|
||||||
|
fun bind(message: MessageRecord, @ColorInt textColor: Int) {
|
||||||
|
assert(message.isDeleted)
|
||||||
|
deleteTitleTextView.text = context.getString(R.string.deleted_message)
|
||||||
|
deleteTitleTextView.setTextColor(textColor)
|
||||||
|
deletedMessageViewIconImageView.imageTintList = ColorStateList.valueOf(textColor)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
}
|
@ -77,7 +77,11 @@ class VisibleMessageContentView : LinearLayout {
|
|||||||
mainContainer.removeAllViews()
|
mainContainer.removeAllViews()
|
||||||
onContentClick = null
|
onContentClick = null
|
||||||
onContentDoubleTap = null
|
onContentDoubleTap = null
|
||||||
if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
if (message.isDeleted) {
|
||||||
|
val deletedMessageView = DeletedMessageView(context)
|
||||||
|
deletedMessageView.bind(message, VisibleMessageContentView.getTextColor(context,message))
|
||||||
|
mainContainer.addView(deletedMessageView)
|
||||||
|
} else if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) {
|
||||||
val linkPreviewView = LinkPreviewView(context)
|
val linkPreviewView = LinkPreviewView(context)
|
||||||
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
|
||||||
mainContainer.addView(linkPreviewView)
|
mainContainer.addView(linkPreviewView)
|
||||||
|
@ -266,6 +266,7 @@ class VisibleMessageView : LinearLayout {
|
|||||||
|
|
||||||
// region Interaction
|
// region Interaction
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
if (onPress == null || onSwipeToReply == null || onLongPress == null) { return false }
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> onDown(event)
|
MotionEvent.ACTION_DOWN -> onDown(event)
|
||||||
MotionEvent.ACTION_MOVE -> onMove(event)
|
MotionEvent.ACTION_MOVE -> onMove(event)
|
||||||
|
@ -12,12 +12,14 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
private val messageIDTable = "loki_message_friend_request_database"
|
private val messageIDTable = "loki_message_friend_request_database"
|
||||||
private val messageThreadMappingTable = "loki_message_thread_mapping_database"
|
private val messageThreadMappingTable = "loki_message_thread_mapping_database"
|
||||||
private val errorMessageTable = "loki_error_message_database"
|
private val errorMessageTable = "loki_error_message_database"
|
||||||
|
private val messageHashTable = "loki_message_hash_database"
|
||||||
private val messageID = "message_id"
|
private val messageID = "message_id"
|
||||||
private val serverID = "server_id"
|
private val serverID = "server_id"
|
||||||
private val friendRequestStatus = "friend_request_status"
|
private val friendRequestStatus = "friend_request_status"
|
||||||
private val threadID = "thread_id"
|
private val threadID = "thread_id"
|
||||||
private val errorMessage = "error_message"
|
private val errorMessage = "error_message"
|
||||||
private val messageType = "message_type"
|
private val messageType = "message_type"
|
||||||
|
private val serverHash = "server_hash"
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@ -28,6 +30,8 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
val updateMessageIDTableForType = "ALTER TABLE $messageIDTable ADD COLUMN $messageType INTEGER DEFAULT 0; ALTER TABLE $messageIDTable ADD CONSTRAINT PK_$messageIDTable PRIMARY KEY ($messageID, $serverID);"
|
val updateMessageIDTableForType = "ALTER TABLE $messageIDTable ADD COLUMN $messageType INTEGER DEFAULT 0; ALTER TABLE $messageIDTable ADD CONSTRAINT PK_$messageIDTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val updateMessageMappingTable = "ALTER TABLE $messageThreadMappingTable ADD COLUMN $serverID INTEGER DEFAULT 0; ALTER TABLE $messageThreadMappingTable ADD CONSTRAINT PK_$messageThreadMappingTable PRIMARY KEY ($messageID, $serverID);"
|
val updateMessageMappingTable = "ALTER TABLE $messageThreadMappingTable ADD COLUMN $serverID INTEGER DEFAULT 0; ALTER TABLE $messageThreadMappingTable ADD CONSTRAINT PK_$messageThreadMappingTable PRIMARY KEY ($messageID, $serverID);"
|
||||||
|
@JvmStatic
|
||||||
|
val createMessageHashTableCommand = "CREATE TABLE IF NOT EXISTS $messageHashTable ($messageID INTEGER PRIMARY KEY, $serverHash STRING);"
|
||||||
|
|
||||||
const val SMS_TYPE = 0
|
const val SMS_TYPE = 0
|
||||||
const val MMS_TYPE = 1
|
const val MMS_TYPE = 1
|
||||||
@ -150,4 +154,24 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
|
|||||||
database.endTransaction()
|
database.endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMessageServerHash(messageID: Long): String? {
|
||||||
|
val database = databaseHelper.readableDatabase
|
||||||
|
return database.get(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) { cursor ->
|
||||||
|
cursor.getString(serverHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMessageServerHash(messageID: Long, serverHash: String) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
val contentValues = ContentValues(2)
|
||||||
|
contentValues.put(Companion.messageID, messageID)
|
||||||
|
contentValues.put(Companion.serverHash, serverHash)
|
||||||
|
database.insertOrUpdate(messageHashTable, contentValues, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMessageServerHash(messageID: Long) {
|
||||||
|
val database = databaseHelper.writableDatabase
|
||||||
|
database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString()))
|
||||||
|
}
|
||||||
}
|
}
|
@ -38,6 +38,10 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||||||
public abstract void markAsSent(long messageId, boolean secure);
|
public abstract void markAsSent(long messageId, boolean secure);
|
||||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||||
|
|
||||||
|
public abstract void markAsDeleted(long messageId, boolean read);
|
||||||
|
|
||||||
|
public abstract boolean deleteMessage(long messageId);
|
||||||
|
|
||||||
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
|
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
|
||||||
try {
|
try {
|
||||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||||
|
@ -391,6 +391,23 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markAsDeleted(long messageId, boolean read) {
|
||||||
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(READ, 1);
|
||||||
|
contentValues.put(BODY, "");
|
||||||
|
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||||
|
|
||||||
|
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||||
|
ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
|
||||||
|
|
||||||
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
|
if (!read) { DatabaseFactory.getThreadDatabase(context).decrementUnread(threadId, 1); }
|
||||||
|
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE, Optional.of(threadId));
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markExpireStarted(long messageId) {
|
public void markExpireStarted(long messageId) {
|
||||||
markExpireStarted(messageId, System.currentTimeMillis());
|
markExpireStarted(messageId, System.currentTimeMillis());
|
||||||
@ -906,7 +923,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
reader.close();
|
reader.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean delete(long messageId) {
|
@Override
|
||||||
|
public boolean deleteMessage(long messageId) {
|
||||||
long threadId = getThreadIdForMessage(messageId);
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||||
ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
|
ThreadUtils.queue(() -> attachmentDatabase.deleteAttachmentsForMessage(messageId));
|
||||||
@ -1033,7 +1051,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null);
|
cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null);
|
||||||
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
delete(cursor.getLong(0));
|
deleteMessage(cursor.getLong(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@ -1059,7 +1077,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||||||
|
|
||||||
while (cursor != null && cursor.moveToNext()) {
|
while (cursor != null && cursor.moveToNext()) {
|
||||||
Log.i("MmsDatabase", "Trimming: " + cursor.getLong(0));
|
Log.i("MmsDatabase", "Trimming: " + cursor.getLong(0));
|
||||||
delete(cursor.getLong(0));
|
deleteMessage(cursor.getLong(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -40,6 +40,7 @@ public interface MmsSmsColumns {
|
|||||||
protected static final long BASE_PENDING_SECURE_SMS_FALLBACK = 25;
|
protected static final long BASE_PENDING_SECURE_SMS_FALLBACK = 25;
|
||||||
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
protected static final long BASE_PENDING_INSECURE_SMS_FALLBACK = 26;
|
||||||
public static final long BASE_DRAFT_TYPE = 27;
|
public static final long BASE_DRAFT_TYPE = 27;
|
||||||
|
protected static final long BASE_DELETED_TYPE = 28;
|
||||||
|
|
||||||
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
|
protected static final long[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
|
||||||
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
|
||||||
@ -152,6 +153,8 @@ public interface MmsSmsColumns {
|
|||||||
return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE;
|
return (type & BASE_TYPE_MASK) == BASE_INBOX_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isDeletedMessage(long type) { return (type & BASE_TYPE_MASK) == BASE_DELETED_TYPE; }
|
||||||
|
|
||||||
public static boolean isJoinedType(long type) {
|
public static boolean isJoinedType(long type) {
|
||||||
return (type & BASE_TYPE_MASK) == JOINED_TYPE;
|
return (type & BASE_TYPE_MASK) == JOINED_TYPE;
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ public class MmsSmsDatabase extends Database {
|
|||||||
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
|
||||||
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
|
||||||
|
|
||||||
return queryTables(PROJECTION, selection, order, "1");
|
return queryTables(PROJECTION, selection, order, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getLastMessageID(long threadId) {
|
public long getLastMessageID(long threadId) {
|
||||||
|
@ -183,6 +183,18 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)});
|
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(id)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markAsDeleted(long messageId, boolean read) {
|
||||||
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
|
ContentValues contentValues = new ContentValues();
|
||||||
|
contentValues.put(READ, 1);
|
||||||
|
contentValues.put(BODY, "");
|
||||||
|
database.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
|
||||||
|
long threadId = getThreadIdForMessage(messageId);
|
||||||
|
if (!read) { DatabaseFactory.getThreadDatabase(context).decrementUnread(threadId, 1); }
|
||||||
|
updateTypeBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_DELETED_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markExpireStarted(long id) {
|
public void markExpireStarted(long id) {
|
||||||
markExpireStarted(id, System.currentTimeMillis());
|
markExpireStarted(id, System.currentTimeMillis());
|
||||||
@ -517,6 +529,7 @@ public class SmsDatabase extends MessagingDatabase {
|
|||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean deleteMessage(long messageId) {
|
public boolean deleteMessage(long messageId) {
|
||||||
Log.i("MessageDatabase", "Deleting: " + messageId);
|
Log.i("MessageDatabase", "Deleting: " + messageId);
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
@ -148,6 +148,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
|
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
|
||||||
JobQueue.shared.add(TrimThreadJob(threadID))
|
JobQueue.shared.add(TrimThreadJob(threadID))
|
||||||
}
|
}
|
||||||
|
message.serverHash?.let { serverHash ->
|
||||||
|
messageID?.let { id ->
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setMessageServerHash(id, serverHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
return messageID
|
return messageID
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,6 +363,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setMessageServerHash(messageID: Long, serverHash: String) {
|
||||||
|
DatabaseFactory.getLokiMessageDatabase(context).setMessageServerHash(messageID, serverHash)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getGroup(groupID: String): GroupRecord? {
|
override fun getGroup(groupID: String): GroupRecord? {
|
||||||
val group = DatabaseFactory.getGroupDatabase(context).getGroup(groupID)
|
val group = DatabaseFactory.getGroupDatabase(context).getGroup(groupID)
|
||||||
return if (group.isPresent) { group.get() } else null
|
return if (group.isPresent) { group.get() } else null
|
||||||
|
@ -294,6 +294,14 @@ public class ThreadDatabase extends Database {
|
|||||||
String.valueOf(threadId)});
|
String.valueOf(threadId)});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void decrementUnread(long threadId, int amount) {
|
||||||
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
db.execSQL("UPDATE " + TABLE_NAME + " SET " + READ + " = 0, " +
|
||||||
|
UNREAD_COUNT + " = " + UNREAD_COUNT + " - ? WHERE " + ID + " = ? AND " + UNREAD_COUNT + " > 0",
|
||||||
|
new String[] {String.valueOf(amount),
|
||||||
|
String.valueOf(threadId)});
|
||||||
|
}
|
||||||
|
|
||||||
public void setDistributionType(long threadId, int distributionType) {
|
public void setDistributionType(long threadId, int distributionType) {
|
||||||
ContentValues contentValues = new ContentValues(1);
|
ContentValues contentValues = new ContentValues(1);
|
||||||
contentValues.put(TYPE, distributionType);
|
contentValues.put(TYPE, distributionType);
|
||||||
@ -536,9 +544,14 @@ public class ThreadDatabase extends Database {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
|
||||||
MessageRecord record;
|
MessageRecord record = null;
|
||||||
|
if (reader != null) {
|
||||||
if (reader != null && (record = reader.getNext()) != null) {
|
record = reader.getNext();
|
||||||
|
while (record != null && record.isDeleted()) {
|
||||||
|
record = reader.getNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record != null && !record.isDeleted()) {
|
||||||
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
|
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
|
||||||
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
|
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
|
||||||
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
|
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());
|
||||||
|
@ -59,9 +59,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
private static final int lokiV25 = 46;
|
private static final int lokiV25 = 46;
|
||||||
private static final int lokiV26 = 47;
|
private static final int lokiV26 = 47;
|
||||||
private static final int lokiV27 = 48;
|
private static final int lokiV27 = 48;
|
||||||
|
private static final int lokiV28 = 49;
|
||||||
|
|
||||||
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
// Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
|
||||||
private static final int DATABASE_VERSION = lokiV27;
|
private static final int DATABASE_VERSION = lokiV28;
|
||||||
private static final String DATABASE_NAME = "signal.db";
|
private static final String DATABASE_NAME = "signal.db";
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
@ -123,6 +124,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
|
||||||
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
|
||||||
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
|
||||||
|
db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand());
|
||||||
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
|
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
|
||||||
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
|
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
|
||||||
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
|
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
|
||||||
@ -302,6 +304,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||||||
db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
|
db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < lokiV28) {
|
||||||
|
db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand());
|
||||||
|
}
|
||||||
|
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction();
|
db.endTransaction();
|
||||||
|
@ -117,6 +117,7 @@ public abstract class DisplayRecord {
|
|||||||
public boolean isMissedCall() {
|
public boolean isMissedCall() {
|
||||||
return SmsDatabase.Types.isMissedCall(type);
|
return SmsDatabase.Types.isMissedCall(type);
|
||||||
}
|
}
|
||||||
|
public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); }
|
||||||
|
|
||||||
public boolean isControlMessage() {
|
public boolean isControlMessage() {
|
||||||
return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification();
|
return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification();
|
||||||
|
@ -49,9 +49,9 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
|
|||||||
// DMs
|
// DMs
|
||||||
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
|
||||||
val dmsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
val dmsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
|
||||||
envelopes.map { envelope ->
|
envelopes.map { (envelope, serverHash) ->
|
||||||
// FIXME: Using a job here seems like a bad idea...
|
// FIXME: Using a job here seems like a bad idea...
|
||||||
MessageReceiveJob(envelope.toByteArray()).executeAsync()
|
MessageReceiveJob(envelope.toByteArray(), serverHash).executeAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
promises.addAll(dmsPromise.get())
|
promises.addAll(dmsPromise.get())
|
||||||
|
@ -229,7 +229,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (expiredMessage != null) {
|
if (expiredMessage != null) {
|
||||||
if (expiredMessage.mms) mmsDatabase.delete(expiredMessage.id);
|
if (expiredMessage.mms) mmsDatabase.deleteMessage(expiredMessage.id);
|
||||||
else smsDatabase.deleteMessage(expiredMessage.id);
|
else smsDatabase.deleteMessage(expiredMessage.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ public class AttachmentUtil {
|
|||||||
.size();
|
.size();
|
||||||
|
|
||||||
if (attachmentCount <= 1) {
|
if (attachmentCount <= 1) {
|
||||||
DatabaseFactory.getMmsDatabase(context).delete(mmsId);
|
DatabaseFactory.getMmsDatabase(context).deleteMessage(mmsId);
|
||||||
} else {
|
} else {
|
||||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
|
DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:behavior_hideable="true"
|
||||||
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deleteForMeTextView"
|
||||||
|
style="@style/BottomSheetActionItem"
|
||||||
|
android:text="@string/delete_message_for_me"
|
||||||
|
android:textColor="@color/destructive"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deleteForEveryoneTextView"
|
||||||
|
style="@style/BottomSheetActionItem"
|
||||||
|
android:text="@string/delete_message_for_everyone"
|
||||||
|
android:textColor="@color/destructive"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/cancelTextView"
|
||||||
|
style="@style/BottomSheetActionItem"
|
||||||
|
android:text="@string/cancel" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
32
app/src/main/res/layout/view_deleted_message.xml
Normal file
32
app/src/main/res/layout/view_deleted_message.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
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_height="wrap_content"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="@dimen/small_spacing"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/deletedMessageViewIconImageView"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginStart="@dimen/small_spacing"
|
||||||
|
android:src="?menu_trash_icon"
|
||||||
|
app:tint="@color/text" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deleteTitleTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:layout_marginEnd="@dimen/small_spacing"
|
||||||
|
android:textSize="@dimen/very_small_font_size"
|
||||||
|
android:textColor="@color/text"
|
||||||
|
tools:text="This message has been deleted"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -893,4 +893,9 @@
|
|||||||
<string name="dialog_send_seed_send_button_title">Send</string>
|
<string name="dialog_send_seed_send_button_title">Send</string>
|
||||||
<string name="notify_type_all">All</string>
|
<string name="notify_type_all">All</string>
|
||||||
<string name="notify_type_mentions">Mentions</string>
|
<string name="notify_type_mentions">Mentions</string>
|
||||||
|
<string name="deleted_message">This message has been deleted</string>
|
||||||
|
<string name="delete_message_for_me">Delete just for me</string>
|
||||||
|
<string name="delete_message_for_everyone">Delete for everyone</string>
|
||||||
|
<string name="delete_message_for_me_and_recipient">Delete for me and %s</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -13,6 +13,8 @@ interface MessageDataProvider {
|
|||||||
fun getMessageID(serverID: Long): Long?
|
fun getMessageID(serverID: Long): Long?
|
||||||
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
fun getMessageID(serverId: Long, threadId: Long): Pair<Long, Boolean>?
|
||||||
fun deleteMessage(messageID: Long, isSms: Boolean)
|
fun deleteMessage(messageID: Long, isSms: Boolean)
|
||||||
|
fun updateMessageAsDeleted(timestamp: Long, author: String)
|
||||||
|
fun getServerHashForMessage(messageID: Long): String?
|
||||||
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
|
||||||
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
|
||||||
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
|
||||||
|
@ -93,6 +93,7 @@ interface StorageProtocol {
|
|||||||
fun markAsSent(timestamp: Long, author: String)
|
fun markAsSent(timestamp: Long, author: String)
|
||||||
fun markUnidentified(timestamp: Long, author: String)
|
fun markUnidentified(timestamp: Long, author: String)
|
||||||
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
|
||||||
|
fun setMessageServerHash(messageID: Long, serverHash: String)
|
||||||
|
|
||||||
// Closed Groups
|
// Closed Groups
|
||||||
fun getGroup(groupID: String): GroupRecord?
|
fun getGroup(groupID: String): GroupRecord?
|
||||||
|
@ -7,7 +7,7 @@ import org.session.libsession.messaging.sending_receiving.handle
|
|||||||
import org.session.libsession.messaging.utilities.Data
|
import org.session.libsession.messaging.utilities.Data
|
||||||
import org.session.libsignal.utilities.Log
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
class MessageReceiveJob(val data: ByteArray, val serverHash: String? = null, val openGroupMessageServerID: Long? = null, val openGroupID: String? = null) : Job {
|
||||||
override var delegate: JobDelegate? = null
|
override var delegate: JobDelegate? = null
|
||||||
override var id: String? = null
|
override var id: String? = null
|
||||||
override var failureCount: Int = 0
|
override var failureCount: Int = 0
|
||||||
@ -21,6 +21,7 @@ class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long?
|
|||||||
|
|
||||||
// Keys used for database storage
|
// Keys used for database storage
|
||||||
private val DATA_KEY = "data"
|
private val DATA_KEY = "data"
|
||||||
|
private val SERVER_HASH_KEY = "serverHash"
|
||||||
private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
|
private val OPEN_GROUP_MESSAGE_SERVER_ID_KEY = "openGroupMessageServerID"
|
||||||
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
private val OPEN_GROUP_ID_KEY = "open_group_id"
|
||||||
}
|
}
|
||||||
@ -34,6 +35,7 @@ class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long?
|
|||||||
try {
|
try {
|
||||||
val isRetry: Boolean = failureCount != 0
|
val isRetry: Boolean = failureCount != 0
|
||||||
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID)
|
val (message, proto) = MessageReceiver.parse(this.data, this.openGroupMessageServerID)
|
||||||
|
message.serverHash = serverHash
|
||||||
synchronized(RECEIVE_LOCK) { // FIXME: Do we need this?
|
synchronized(RECEIVE_LOCK) { // FIXME: Do we need this?
|
||||||
MessageReceiver.handle(message, proto, this.openGroupID)
|
MessageReceiver.handle(message, proto, this.openGroupID)
|
||||||
}
|
}
|
||||||
@ -67,6 +69,7 @@ class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long?
|
|||||||
|
|
||||||
override fun serialize(): Data {
|
override fun serialize(): Data {
|
||||||
val builder = Data.Builder().putByteArray(DATA_KEY, data)
|
val builder = Data.Builder().putByteArray(DATA_KEY, data)
|
||||||
|
serverHash?.let { builder.putString(SERVER_HASH_KEY, it) }
|
||||||
openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
|
openGroupMessageServerID?.let { builder.putLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY, it) }
|
||||||
openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
|
openGroupID?.let { builder.putString(OPEN_GROUP_ID_KEY, it) }
|
||||||
return builder.build();
|
return builder.build();
|
||||||
@ -81,6 +84,7 @@ class MessageReceiveJob(val data: ByteArray, val openGroupMessageServerID: Long?
|
|||||||
override fun create(data: Data): MessageReceiveJob {
|
override fun create(data: Data): MessageReceiveJob {
|
||||||
return MessageReceiveJob(
|
return MessageReceiveJob(
|
||||||
data.getByteArray(DATA_KEY),
|
data.getByteArray(DATA_KEY),
|
||||||
|
data.getString(SERVER_HASH_KEY),
|
||||||
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
|
data.getLong(OPEN_GROUP_MESSAGE_SERVER_ID_KEY),
|
||||||
data.getString(OPEN_GROUP_ID_KEY)
|
data.getString(OPEN_GROUP_ID_KEY)
|
||||||
)
|
)
|
||||||
|
@ -13,6 +13,7 @@ abstract class Message {
|
|||||||
var sender: String? = null
|
var sender: String? = null
|
||||||
var groupPublicKey: String? = null
|
var groupPublicKey: String? = null
|
||||||
var openGroupServerMessageID: Long? = null
|
var openGroupServerMessageID: Long? = null
|
||||||
|
var serverHash: String? = null
|
||||||
|
|
||||||
open val ttl: Long = 14 * 24 * 60 * 60 * 1000
|
open val ttl: Long = 14 * 24 * 60 * 60 * 1000
|
||||||
open val isSelfSendValid: Boolean = false
|
open val isSelfSendValid: Boolean = false
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
package org.session.libsession.messaging.messages.control
|
||||||
|
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
|
||||||
|
class UnsendRequest(): ControlMessage() {
|
||||||
|
var timestamp: Long? = null
|
||||||
|
var author: String? = null
|
||||||
|
|
||||||
|
override val isSelfSendValid: Boolean = true
|
||||||
|
|
||||||
|
// region Validation
|
||||||
|
override fun isValid(): Boolean {
|
||||||
|
if (!super.isValid()) return false
|
||||||
|
return timestamp != null && author != null
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "UnsendRequest"
|
||||||
|
|
||||||
|
fun fromProto(proto: SignalServiceProtos.Content): UnsendRequest? {
|
||||||
|
val unsendRequestProto = if (proto.hasUnsendRequest()) proto.unsendRequest else return null
|
||||||
|
val timestamp = unsendRequestProto.timestamp
|
||||||
|
val author = unsendRequestProto.author
|
||||||
|
return UnsendRequest(timestamp, author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(timestamp: Long, author: String) : this() {
|
||||||
|
this.timestamp = timestamp
|
||||||
|
this.author = author
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toProto(): SignalServiceProtos.Content? {
|
||||||
|
val timestamp = timestamp
|
||||||
|
val author = author
|
||||||
|
if (timestamp == null || author == null) {
|
||||||
|
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val unsendRequestProto = SignalServiceProtos.UnsendRequest.newBuilder()
|
||||||
|
unsendRequestProto.timestamp = timestamp
|
||||||
|
unsendRequestProto.author = author
|
||||||
|
val contentProto = SignalServiceProtos.Content.newBuilder()
|
||||||
|
try {
|
||||||
|
contentProto.unsendRequest = unsendRequestProto.build()
|
||||||
|
return contentProto.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Couldn't construct unsend request proto from: $this")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -94,6 +94,7 @@ object MessageReceiver {
|
|||||||
DataExtractionNotification.fromProto(proto) ?:
|
DataExtractionNotification.fromProto(proto) ?:
|
||||||
ExpirationTimerUpdate.fromProto(proto) ?:
|
ExpirationTimerUpdate.fromProto(proto) ?:
|
||||||
ConfigurationMessage.fromProto(proto) ?:
|
ConfigurationMessage.fromProto(proto) ?:
|
||||||
|
UnsendRequest.fromProto(proto) ?:
|
||||||
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage
|
||||||
// Ignore self send if needed
|
// Ignore self send if needed
|
||||||
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend
|
||||||
|
@ -11,6 +11,7 @@ import org.session.libsession.messaging.messages.Message
|
|||||||
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
|
||||||
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
import org.session.libsession.messaging.messages.control.ConfigurationMessage
|
||||||
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
|
||||||
|
import org.session.libsession.messaging.messages.control.UnsendRequest
|
||||||
import org.session.libsession.messaging.messages.visible.*
|
import org.session.libsession.messaging.messages.visible.*
|
||||||
import org.session.libsession.messaging.open_groups.*
|
import org.session.libsession.messaging.open_groups.*
|
||||||
import org.session.libsession.messaging.utilities.MessageWrapper
|
import org.session.libsession.messaging.utilities.MessageWrapper
|
||||||
@ -94,7 +95,7 @@ object MessageSender {
|
|||||||
// • a closed group control message of type `new`
|
// • a closed group control message of type `new`
|
||||||
var isNewClosedGroupControlMessage = false
|
var isNewClosedGroupControlMessage = false
|
||||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true
|
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) isNewClosedGroupControlMessage = true
|
||||||
if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage) {
|
if (isSelfSend && message !is ConfigurationMessage && !isSyncMessage && !isNewClosedGroupControlMessage && message !is UnsendRequest) {
|
||||||
handleSuccessfulMessageSend(message, destination)
|
handleSuccessfulMessageSend(message, destination)
|
||||||
deferred.resolve(Unit)
|
deferred.resolve(Unit)
|
||||||
return promise
|
return promise
|
||||||
@ -161,8 +162,10 @@ object MessageSender {
|
|||||||
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
|
||||||
SnodeModule.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
|
SnodeModule.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
|
||||||
}
|
}
|
||||||
|
val hash = it["hash"] as? String
|
||||||
|
message.serverHash = hash
|
||||||
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
handleSuccessfulMessageSend(message, destination, isSyncMessage)
|
||||||
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
|
var shouldNotify = ((message is VisibleMessage || message is UnsendRequest) && !isSyncMessage)
|
||||||
/*
|
/*
|
||||||
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) {
|
||||||
shouldNotify = true
|
shouldNotify = true
|
||||||
@ -251,28 +254,35 @@ object MessageSender {
|
|||||||
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false, openGroupSentTimestamp: Long = -1) {
|
||||||
val storage = MessagingModuleConfiguration.shared.storage
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
val userPublicKey = storage.getUserPublicKey()!!
|
val userPublicKey = storage.getUserPublicKey()!!
|
||||||
val messageID = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
|
|
||||||
// Ignore future self-sends
|
// Ignore future self-sends
|
||||||
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
|
||||||
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey)?.let { messageID ->
|
||||||
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
if (openGroupSentTimestamp != -1L && message is VisibleMessage) {
|
||||||
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
storage.addReceivedMessageTimestamp(openGroupSentTimestamp)
|
||||||
message.sentTimestamp = openGroupSentTimestamp
|
storage.updateSentTimestamp(messageID, message.isMediaMessage(), openGroupSentTimestamp, message.threadID!!)
|
||||||
}
|
message.sentTimestamp = openGroupSentTimestamp
|
||||||
// Track the open group server message ID
|
}
|
||||||
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
// When the sync message is successfully sent, the hash value of this TSOutgoingMessage
|
||||||
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
// will be replaced by the hash value of the sync message. Since the hash value of the
|
||||||
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
// real message has no use when we delete a message. It is OK to let it be.
|
||||||
if (threadID != null && threadID >= 0) {
|
message.serverHash?.let {
|
||||||
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
storage.setMessageServerHash(messageID, it)
|
||||||
|
}
|
||||||
|
// Track the open group server message ID
|
||||||
|
if (message.openGroupServerMessageID != null && destination is Destination.OpenGroupV2) {
|
||||||
|
val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray())
|
||||||
|
val threadID = storage.getThreadId(Address.fromSerialized(encoded))
|
||||||
|
if (threadID != null && threadID >= 0) {
|
||||||
|
storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mark the message as sent
|
||||||
|
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
|
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
|
// Start the disappearing messages timer if needed
|
||||||
|
if (message is VisibleMessage && !isSyncMessage) {
|
||||||
|
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Mark the message as sent
|
|
||||||
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
|
|
||||||
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
|
|
||||||
// Start the disappearing messages timer if needed
|
|
||||||
if (message is VisibleMessage && !isSyncMessage) {
|
|
||||||
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
|
|
||||||
}
|
}
|
||||||
// Sync the message if:
|
// Sync the message if:
|
||||||
// • it's a visible message
|
// • it's a visible message
|
||||||
|
@ -14,6 +14,7 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
|||||||
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
|
||||||
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
|
||||||
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.GroupRecord
|
import org.session.libsession.utilities.GroupRecord
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
@ -49,6 +50,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
|
|||||||
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message)
|
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message)
|
||||||
is DataExtractionNotification -> handleDataExtractionNotification(message)
|
is DataExtractionNotification -> handleDataExtractionNotification(message)
|
||||||
is ConfigurationMessage -> handleConfigurationMessage(message)
|
is ConfigurationMessage -> handleConfigurationMessage(message)
|
||||||
|
is UnsendRequest -> handleUnsendRequest(message)
|
||||||
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -145,6 +147,23 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) {
|
|||||||
}
|
}
|
||||||
storage.addContacts(message.contacts)
|
storage.addContacts(message.contacts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MessageReceiver.handleUnsendRequest(message: UnsendRequest) {
|
||||||
|
if (message.sender != message.author) { return }
|
||||||
|
val context = MessagingModuleConfiguration.shared.context
|
||||||
|
val storage = MessagingModuleConfiguration.shared.storage
|
||||||
|
val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
|
||||||
|
val timestamp = message.timestamp ?: return
|
||||||
|
val author = message.author ?: return
|
||||||
|
val messageIdToDelete = storage.getMessageIdInDatabase(timestamp, author) ?: return
|
||||||
|
messageDataProvider.getServerHashForMessage(messageIdToDelete)?.let { serverHash ->
|
||||||
|
SnodeAPI.deleteMessage(author, listOf(serverHash))
|
||||||
|
}
|
||||||
|
messageDataProvider.updateMessageAsDeleted(timestamp, author)
|
||||||
|
if (!messageDataProvider.isOutgoingMessage(messageIdToDelete)) {
|
||||||
|
SSKEnvironment.shared.notificationManager.updateNotification(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
// region Visible Messages
|
// region Visible Messages
|
||||||
|
@ -102,8 +102,8 @@ class ClosedGroupPollerV2 {
|
|||||||
}
|
}
|
||||||
promise.success { envelopes ->
|
promise.success { envelopes ->
|
||||||
if (!isPolling(groupPublicKey)) { return@success }
|
if (!isPolling(groupPublicKey)) { return@success }
|
||||||
envelopes.forEach { envelope ->
|
envelopes.forEach { (envelope, serverHash) ->
|
||||||
val job = MessageReceiveJob(envelope.toByteArray())
|
val job = MessageReceiveJob(envelope.toByteArray(), serverHash)
|
||||||
JobQueue.shared.add(job)
|
JobQueue.shared.add(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,8 +91,8 @@ class Poller {
|
|||||||
task { Unit } // The long polling connection has been canceled; don't recurse
|
task { Unit } // The long polling connection has been canceled; don't recurse
|
||||||
} else {
|
} else {
|
||||||
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
val messages = SnodeAPI.parseRawMessagesResponse(rawResponse, snode, userPublicKey)
|
||||||
messages.forEach { envelope ->
|
messages.forEach { (envelope, serverHash) ->
|
||||||
val job = MessageReceiveJob(envelope.toByteArray())
|
val job = MessageReceiveJob(envelope.toByteArray(), serverHash)
|
||||||
JobQueue.shared.add(job)
|
JobQueue.shared.add(job)
|
||||||
}
|
}
|
||||||
poll(snode, deferred)
|
poll(snode, deferred)
|
||||||
|
@ -52,7 +52,7 @@ object SnodeAPI {
|
|||||||
if (useTestnet) {
|
if (useTestnet) {
|
||||||
setOf( "http://public.loki.foundation:38157" )
|
setOf( "http://public.loki.foundation:38157" )
|
||||||
} else {
|
} else {
|
||||||
setOf( "https://storage.seed1.loki.network:$seedNodePort ", "https://storage.seed3.loki.network:$seedNodePort ", "https://public.loki.foundation:$seedNodePort" )
|
setOf( "https://storage.seed1.loki.network:$seedNodePort", "https://storage.seed3.loki.network:$seedNodePort", "https://public.loki.foundation:$seedNodePort" )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val snodeFailureThreshold = 3
|
private val snodeFailureThreshold = 3
|
||||||
@ -329,6 +329,50 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteMessage(publicKey: String, serverHashes: List<String>): Promise<Map<String,Boolean>, Exception> {
|
||||||
|
return retryIfNeeded(maxRetryCount) {
|
||||||
|
val module = MessagingModuleConfiguration.shared
|
||||||
|
val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
||||||
|
val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair)
|
||||||
|
getSingleTargetSnode(publicKey).bind { snode ->
|
||||||
|
retryIfNeeded(maxRetryCount) {
|
||||||
|
val signature = ByteArray(Sign.BYTES)
|
||||||
|
val verificationData = (Snode.Method.DeleteMessage.rawValue + serverHashes.fold("") { a, v -> a + v }).toByteArray()
|
||||||
|
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
|
||||||
|
val deleteMessageParams = mapOf(
|
||||||
|
"pubkey" to userPublicKey,
|
||||||
|
"pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString,
|
||||||
|
"messages" to serverHashes,
|
||||||
|
"signature" to Base64.encodeBytes(signature)
|
||||||
|
)
|
||||||
|
invoke(Snode.Method.DeleteMessage, snode, publicKey, deleteMessageParams).map { rawResponse ->
|
||||||
|
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
|
||||||
|
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
|
||||||
|
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
|
||||||
|
val isFailed = json["failed"] as? Boolean ?: false
|
||||||
|
val statusCode = json["code"] as? String
|
||||||
|
val reason = json["reason"] as? String
|
||||||
|
hexSnodePublicKey to if (isFailed) {
|
||||||
|
Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).")
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val hashes = json["deleted"] as List<String> // Hashes of deleted messages
|
||||||
|
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 = (userPublicKey + serverHashes.fold("") { a, v -> a + v } + hashes.fold("") { a, v -> a + v }).toByteArray()
|
||||||
|
sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@map result.toMap()
|
||||||
|
}.fail { e ->
|
||||||
|
Log.e("Loki", "Failed to delete messages", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parsing
|
// Parsing
|
||||||
private fun parseSnodes(rawResponse: Any): List<Snode> {
|
private fun parseSnodes(rawResponse: Any): List<Snode> {
|
||||||
val json = rawResponse as? Map<*, *>
|
val json = rawResponse as? Map<*, *>
|
||||||
@ -382,7 +426,7 @@ object SnodeAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<SignalServiceProtos.Envelope> {
|
fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
||||||
val messages = rawResponse["messages"] as? List<*>
|
val messages = rawResponse["messages"] as? List<*>
|
||||||
return if (messages != null) {
|
return if (messages != null) {
|
||||||
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
updateLastMessageHashValueIfPossible(snode, publicKey, messages)
|
||||||
@ -421,14 +465,14 @@ object SnodeAPI {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseEnvelopes(rawMessages: List<*>): List<SignalServiceProtos.Envelope> {
|
private fun parseEnvelopes(rawMessages: List<*>): List<Pair<SignalServiceProtos.Envelope, String?>> {
|
||||||
return rawMessages.mapNotNull { rawMessage ->
|
return rawMessages.mapNotNull { rawMessage ->
|
||||||
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
val rawMessageAsJSON = rawMessage as? Map<*, *>
|
||||||
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
val base64EncodedData = rawMessageAsJSON?.get("data") as? String
|
||||||
val data = base64EncodedData?.let { Base64.decode(it) }
|
val data = base64EncodedData?.let { Base64.decode(it) }
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
try {
|
try {
|
||||||
MessageWrapper.unwrap(data)
|
Pair(MessageWrapper.unwrap(data), rawMessageAsJSON.get("hash") as? String)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.")
|
Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.")
|
||||||
null
|
null
|
||||||
@ -524,5 +568,5 @@ object SnodeAPI {
|
|||||||
|
|
||||||
// Type Aliases
|
// Type Aliases
|
||||||
typealias RawResponse = Map<*, *>
|
typealias RawResponse = Map<*, *>
|
||||||
typealias MessageListPromise = Promise<List<SignalServiceProtos.Envelope>, Exception>
|
typealias MessageListPromise = Promise<List<Pair<SignalServiceProtos.Envelope, String?>>, Exception>
|
||||||
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
typealias RawResponsePromise = Promise<RawResponse, Exception>
|
||||||
|
@ -35,12 +35,20 @@ message TypingMessage {
|
|||||||
required Action action = 2;
|
required Action action = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UnsendRequest {
|
||||||
|
// @required
|
||||||
|
required uint64 timestamp = 1;
|
||||||
|
// @required
|
||||||
|
required string author = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message Content {
|
message Content {
|
||||||
optional DataMessage dataMessage = 1;
|
optional DataMessage dataMessage = 1;
|
||||||
optional ReceiptMessage receiptMessage = 5;
|
optional ReceiptMessage receiptMessage = 5;
|
||||||
optional TypingMessage typingMessage = 6;
|
optional TypingMessage typingMessage = 6;
|
||||||
optional ConfigurationMessage configurationMessage = 7;
|
optional ConfigurationMessage configurationMessage = 7;
|
||||||
optional DataExtractionNotification dataExtractionNotification = 8;
|
optional DataExtractionNotification dataExtractionNotification = 8;
|
||||||
|
optional UnsendRequest unsendRequest = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message KeyPair {
|
message KeyPair {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
|
|||||||
GetSwarm("get_snodes_for_pubkey"),
|
GetSwarm("get_snodes_for_pubkey"),
|
||||||
GetMessages("retrieve"),
|
GetMessages("retrieve"),
|
||||||
SendMessage("store"),
|
SendMessage("store"),
|
||||||
|
DeleteMessage("delete"),
|
||||||
OxenDaemonRPCCall("oxend_request"),
|
OxenDaemonRPCCall("oxend_request"),
|
||||||
Info("info"),
|
Info("info"),
|
||||||
DeleteAll("delete_all")
|
DeleteAll("delete_all")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user