Merge remote-tracking branch 'upstream/dev' into conversation_camera_improvement

This commit is contained in:
Harris
2021-08-18 12:12:02 +10:00
42 changed files with 1603 additions and 173 deletions

View File

@@ -19,6 +19,7 @@ import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.Database
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.MessagingDatabase
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.events.PartProgressEvent
import org.thoughtcrime.securesms.mms.MediaConstraints
@@ -167,14 +168,28 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper)
}
override fun deleteMessage(messageID: Long, isSms: Boolean) {
if (isSms) {
val db = DatabaseFactory.getSmsDatabase(context)
db.deleteMessage(messageID)
} else {
val db = DatabaseFactory.getMmsDatabase(context)
db.delete(messageID)
}
val messagingDatabase: MessagingDatabase = if (isSms) DatabaseFactory.getSmsDatabase(context)
else DatabaseFactory.getMmsDatabase(context)
messagingDatabase.deleteMessage(messageID)
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? {

View File

@@ -50,6 +50,7 @@ import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.mentions.MentionsManager
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.OutgoingTextMessage
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.link_preview.LinkPreview
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.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences
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.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.audio.AudioRecorder
@@ -205,6 +209,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
const val PICK_GIF = 10
const val PICK_FROM_LIBRARY = 12
const val INVITE_CONTACTS = 124
//flag
const val IS_UNSEND_REQUESTS_ENABLED = false
}
// endregion
@@ -1110,7 +1117,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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 messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2)
@@ -1137,7 +1198,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} else {
for (message in messages) {
if (message.isMms) {
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).delete(message.id)
DatabaseFactory.getMmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
} else {
DatabaseFactory.getSmsDatabase(this@ConversationActivityV2).deleteMessage(message.id)
}
@@ -1152,6 +1213,72 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
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>) {
val builder = AlertDialog.Builder(this)
val sessionID = messages.first().individualRecipient.address.toString()

View File

@@ -73,9 +73,11 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr
val position = viewHolder.adapterPosition
view.indexInAdapter = position
view.bind(message, getMessageBefore(position, cursor), getMessageAfter(position, cursor), glide, searchQuery)
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
if (!message.isDeleted) {
view.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, view, event) }
view.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) }
view.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) }
}
view.contentViewDelegate = visibleMessageContentViewDelegate
}
is ControlMessageViewHolder -> viewHolder.view.bind(message)

View File

@@ -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)
}
}

View File

@@ -21,13 +21,14 @@ import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.MediaPreviewActivity
import org.thoughtcrime.securesms.components.CornerMask
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView
import org.thoughtcrime.securesms.conversation.v2.utilities.KThumbnailView
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.longmessage.LongMessageActivity
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.video.exo.AttachmentDataSource
import org.thoughtcrime.securesms.util.ActivityDispatcher
import kotlin.math.roundToInt
class AlbumThumbnailView : FrameLayout {
@@ -80,6 +81,13 @@ class AlbumThumbnailView : FrameLayout {
}
return
}
val intersectedSpans = albumCellBodyText.getIntersectedModalSpans(eventRect)
if (intersectedSpans.isNotEmpty()) {
intersectedSpans.forEach { span ->
span.onClick(albumCellBodyText)
}
return
}
// test each album child
albumCellContainer.findViewById<ViewGroup>(R.id.album_thumbnail_root)?.children?.forEachIndexed { index, child ->
child.getGlobalVisibleRect(testRect)
@@ -130,7 +138,8 @@ class AlbumThumbnailView : FrameLayout {
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message)
}
albumCellBodyParent.isVisible = message.body.isNotEmpty()
albumCellBodyText.text = message.body
val body = VisibleMessageContentView.getBodySpans(context, message, null)
albumCellBodyText.text = body
post {
// post to await layout of text
albumCellBodyText.layout?.let { layout ->

View File

@@ -7,6 +7,7 @@ import android.view.MenuItem
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.DatabaseFactory
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 userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
fun userCanDeleteSelectedItems(): Boolean {
if (openGroup == null) { return true }
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 }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
}

View File

@@ -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
}

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.style.BackgroundColorSpan
import android.text.style.ForegroundColorSpan
import android.text.style.URLSpan
@@ -28,21 +29,21 @@ import okhttp3.HttpUrl
import org.session.libsession.utilities.ThemeUtil
import org.session.libsession.utilities.ViewUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.components.AlbumThumbnailView
import org.thoughtcrime.securesms.conversation.v2.dialogs.OpenURLDialog
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities
import org.thoughtcrime.securesms.conversation.v2.utilities.ModalURLSpan
import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.getIntersectedModalSpans
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.util.*
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.SearchUtil
import org.thoughtcrime.securesms.util.SearchUtil.StyleFactory
import org.thoughtcrime.securesms.util.UiModeUtilities
import java.net.IDN
import org.thoughtcrime.securesms.util.getColorWithID
import org.thoughtcrime.securesms.util.toPx
import java.util.*
import kotlin.math.roundToInt
@@ -76,7 +77,11 @@ class VisibleMessageContentView : LinearLayout {
mainContainer.removeAllViews()
onContentClick = 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)
linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster, searchQuery)
mainContainer.addView(linkPreviewView)
@@ -106,6 +111,10 @@ class VisibleMessageContentView : LinearLayout {
quoteView.getGlobalVisibleRect(r)
if (r.contains(event.rawX.roundToInt(), event.rawY.roundToInt())) {
delegate?.scrollToMessageIfPossible(quote.id)
} else {
bodyTextView.getIntersectedModalSpans(event).forEach { span ->
span.onClick(bodyTextView)
}
}
}
} else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) {
@@ -209,7 +218,18 @@ class VisibleMessageContentView : LinearLayout {
val color = getTextColor(context, message)
result.setTextColor(color)
result.setLinkTextColor(color)
val body = getBodySpans(context, message, searchQuery)
result.text = body
return result
}
fun getBodySpans(context: Context, message: MessageRecord, searchQuery: String?): Spannable {
var body = message.body.toSpannable()
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
Linkify.addLinks(body, Linkify.WEB_URLS)
// replace URLSpans with ModalURLSpans
@@ -225,13 +245,7 @@ class VisibleMessageContentView : LinearLayout {
body.removeSpan(urlSpan)
body.setSpan(replacementSpan, start, end, flags)
}
body = MentionUtilities.highlightMentions(body, message.isOutgoing, message.threadId, context)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { BackgroundColorSpan(Color.WHITE) }, body, searchQuery)
body = SearchUtil.getHighlightedSpan(Locale.getDefault(), StyleFactory { ForegroundColorSpan(Color.BLACK) }, body, searchQuery)
result.text = body
return result
return body
}
@ColorInt

View File

@@ -266,6 +266,7 @@ class VisibleMessageView : LinearLayout {
// region Interaction
override fun onTouchEvent(event: MotionEvent): Boolean {
if (onPress == null || onSwipeToReply == null || onLongPress == null) { return false }
when (event.action) {
MotionEvent.ACTION_DOWN -> onDown(event)
MotionEvent.ACTION_MOVE -> onMove(event)

View File

@@ -12,12 +12,14 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
private val messageIDTable = "loki_message_friend_request_database"
private val messageThreadMappingTable = "loki_message_thread_mapping_database"
private val errorMessageTable = "loki_error_message_database"
private val messageHashTable = "loki_message_hash_database"
private val messageID = "message_id"
private val serverID = "server_id"
private val friendRequestStatus = "friend_request_status"
private val threadID = "thread_id"
private val errorMessage = "error_message"
private val messageType = "message_type"
private val serverHash = "server_hash"
@JvmStatic
val createMessageIDTableCommand = "CREATE TABLE $messageIDTable ($messageID INTEGER PRIMARY KEY, $serverID INTEGER DEFAULT 0, $friendRequestStatus INTEGER DEFAULT 0);"
@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);"
@JvmStatic
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 MMS_TYPE = 1
@@ -150,4 +154,24 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
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()))
}
}

View File

@@ -38,6 +38,10 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract void markAsSent(long messageId, boolean secure);
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) {
try {
addToDocument(messageId, MISMATCHED_IDENTITIES,

View File

@@ -391,6 +391,23 @@ public class MmsDatabase extends MessagingDatabase {
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
public void markExpireStarted(long messageId) {
markExpireStarted(messageId, System.currentTimeMillis());
@@ -906,7 +923,8 @@ public class MmsDatabase extends MessagingDatabase {
reader.close();
}
public boolean delete(long messageId) {
@Override
public boolean deleteMessage(long messageId) {
long threadId = getThreadIdForMessage(messageId);
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
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);
while (cursor != null && cursor.moveToNext()) {
delete(cursor.getLong(0));
deleteMessage(cursor.getLong(0));
}
} finally {
@@ -1059,7 +1077,7 @@ public class MmsDatabase extends MessagingDatabase {
while (cursor != null && cursor.moveToNext()) {
Log.i("MmsDatabase", "Trimming: " + cursor.getLong(0));
delete(cursor.getLong(0));
deleteMessage(cursor.getLong(0));
}
} finally {

View File

@@ -40,6 +40,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[] OUTGOING_MESSAGE_TYPES = {BASE_OUTBOX_TYPE, BASE_SENT_TYPE,
BASE_SENDING_TYPE, BASE_SENT_FAILED_TYPE,
@@ -152,6 +153,8 @@ 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 isJoinedType(long type) {
return (type & BASE_TYPE_MASK) == JOINED_TYPE;
}

View File

@@ -129,7 +129,7 @@ public class MmsSmsDatabase extends Database {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
return queryTables(PROJECTION, selection, order, "1");
return queryTables(PROJECTION, selection, order, null);
}
public long getLastMessageID(long threadId) {

View File

@@ -183,6 +183,18 @@ public class SmsDatabase extends MessagingDatabase {
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
public void markExpireStarted(long id) {
markExpireStarted(id, System.currentTimeMillis());
@@ -517,6 +529,7 @@ public class SmsDatabase extends MessagingDatabase {
return cursor;
}
@Override
public boolean deleteMessage(long messageId) {
Log.i("MessageDatabase", "Deleting: " + messageId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();

View File

@@ -148,6 +148,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
if (openGroupID.isNullOrEmpty() && threadID != null && threadID >= 0) {
JobQueue.shared.add(TrimThreadJob(threadID))
}
message.serverHash?.let { serverHash ->
messageID?.let { id ->
DatabaseFactory.getLokiMessageDatabase(context).setMessageServerHash(id, serverHash)
}
}
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? {
val group = DatabaseFactory.getGroupDatabase(context).getGroup(groupID)
return if (group.isPresent) { group.get() } else null

View File

@@ -294,6 +294,14 @@ public class ThreadDatabase extends Database {
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) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(TYPE, distributionType);
@@ -536,9 +544,14 @@ public class ThreadDatabase extends Database {
try {
reader = mmsSmsDatabase.readerFor(mmsSmsDatabase.getConversationSnippet(threadId));
MessageRecord record;
if (reader != null && (record = reader.getNext()) != null) {
MessageRecord record = null;
if (reader != null) {
record = reader.getNext();
while (record != null && record.isDeleted()) {
record = reader.getNext();
}
}
if (record != null && !record.isDeleted()) {
updateThread(threadId, count, getFormattedBodyFor(record), getAttachmentUriFor(record),
record.getTimestamp(), record.getDeliveryStatus(), record.getDeliveryReceiptCount(),
record.getType(), unarchive, record.getExpiresIn(), record.getReadReceiptCount());

View File

@@ -59,9 +59,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int lokiV25 = 46;
private static final int lokiV26 = 47;
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
private static final int DATABASE_VERSION = lokiV27;
private static final int DATABASE_VERSION = lokiV28;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -123,6 +124,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageToThreadMappingTableCommand());
db.execSQL(LokiMessageDatabase.getCreateErrorMessageTableCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand());
db.execSQL(LokiThreadDatabase.getCreateSessionResetTableCommand());
db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand());
db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand());
@@ -302,6 +304,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
}
if (oldVersion < lokiV28) {
db.execSQL(LokiMessageDatabase.getCreateMessageHashTableCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -117,6 +117,7 @@ public abstract class DisplayRecord {
public boolean isMissedCall() {
return SmsDatabase.Types.isMissedCall(type);
}
public boolean isDeleted() { return MmsSmsColumns.Types.isDeletedMessage(type); }
public boolean isControlMessage() {
return isGroupUpdateMessage() || isExpirationTimerUpdate() || isDataExtractionNotification();

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.view.MenuItem;
import android.widget.TextView;
@@ -15,7 +16,7 @@ import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentView;
import network.loki.messenger.R;
@@ -81,18 +82,10 @@ public class LongMessageActivity extends PassphraseRequiredActionBarActivity {
String name = Util.getFirstNonEmpty(recipient.getName(), recipient.getProfileName(), recipient.getAddress().serialize());
getSupportActionBar().setTitle(getString(R.string.LongMessageActivity_message_from_s, name));
}
String trimmedBody = getTrimmedBody(message.get().getFullBody());
String mentionBody = MentionUtilities.highlightMentions(trimmedBody, message.get().getMessageRecord().getThreadId(), this);
textBody.setText(mentionBody);
Spannable bodySpans = VisibleMessageContentView.Companion.getBodySpans(this, message.get().getMessageRecord(), null);
textBody.setText(bodySpans);
textBody.setMovementMethod(LinkMovementMethod.getInstance());
});
}
private String getTrimmedBody(@NonNull String text) {
return text.length() <= MAX_DISPLAY_LENGTH ? text
: text.substring(0, MAX_DISPLAY_LENGTH);
}
}

View File

@@ -49,9 +49,9 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
// DMs
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val dmsPromise = SnodeAPI.getMessages(userPublicKey).map { envelopes ->
envelopes.map { envelope ->
envelopes.map { (envelope, serverHash) ->
// FIXME: Using a job here seems like a bad idea...
MessageReceiveJob(envelope.toByteArray()).executeAsync()
MessageReceiveJob(envelope.toByteArray(), serverHash).executeAsync()
}
}
promises.addAll(dmsPromise.get())

View File

@@ -229,7 +229,7 @@ public class ExpiringMessageManager implements SSKEnvironment.MessageExpirationM
}
if (expiredMessage != null) {
if (expiredMessage.mms) mmsDatabase.delete(expiredMessage.id);
if (expiredMessage.mms) mmsDatabase.deleteMessage(expiredMessage.id);
else smsDatabase.deleteMessage(expiredMessage.id);
}
}

View File

@@ -66,7 +66,7 @@ public class AttachmentUtil {
.size();
if (attachmentCount <= 1) {
DatabaseFactory.getMmsDatabase(context).delete(mmsId);
DatabaseFactory.getMmsDatabase(context).deleteMessage(mmsId);
} else {
DatabaseFactory.getAttachmentDatabase(context).deleteAttachment(attachmentId);
}

View File

@@ -52,7 +52,7 @@
android:layout_width="@dimen/accent_line_thickness"
android:layout_height="match_parent"
android:background="@color/accent"/>
<TextView
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:maxLines="4"
android:ellipsize="end"
android:id="@+id/albumCellBodyText"

View File

@@ -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>

View File

@@ -12,6 +12,8 @@
<TextView
android:textSize="@dimen/text_size"
android:focusable="true"
android:textColorLink="@color/text"
android:id="@+id/longmessage_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

View 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>

View File

@@ -893,4 +893,9 @@
<string name="dialog_send_seed_send_button_title">Send</string>
<string name="notify_type_all">All</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>