diff --git a/app/build.gradle b/app/build.gradle index 647b5a4c4..addf8c5c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -153,8 +153,8 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 218 -def canonicalVersionName = "1.11.8" +def canonicalVersionCode = 221 +def canonicalVersionName = "1.11.9" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt index 43652b09e..46db01b13 100644 --- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt @@ -55,11 +55,15 @@ class HomeActivityTests { onView(newConversationButtonWithDrawable(R.drawable.ic_plus)).perform(ViewActions.click()) onView(newConversationButtonWithDrawable(R.drawable.ic_message)).perform(ViewActions.click()) // new chat + onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard()) onView(withId(R.id.copyButton)).perform(ViewActions.click()) val context = InstrumentationRegistry.getInstrumentation().targetContext - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val copied = clipboardManager.primaryClip!!.getItemAt(0).text - onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied.toString())) + lateinit var copied: String + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + copied = clipboardManager.primaryClip!!.getItemAt(0).text.toString() + } + onView(withId(R.id.publicKeyEditText)).perform(ViewActions.typeText(copied)) onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 5a13830bd..e2f78eecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -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? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 4a86d5edc..c25d157bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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 @@ -241,7 +248,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe searchBottomBar.setEventListener(this) setUpSearchResultObserver() scrollToFirstUnreadMessageIfNeeded() - markAllAsRead() showOrHideInputIfNeeded() if (this.thread.isOpenGroupRecipient) { val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) @@ -255,6 +261,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun onResume() { super.onResume() ApplicationContext.getInstance(this).messageNotifier.setVisibleThread(threadID) + markAllAsRead() } override fun onPause() { @@ -498,7 +505,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } else { MarkReadReceiver.process(this, messages) } - ApplicationContext.getInstance(this).messageNotifier.updateNotification(this, threadID) + ApplicationContext.getInstance(this).messageNotifier.updateNotification(this, false, 0) } override fun inputBarHeightChanged(newValue: Int) { @@ -977,7 +984,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun showCamera() { - attachmentManager.capturePhoto(this, ConversationActivityV2.TAKE_PHOTO) + attachmentManager.capturePhoto(this, ConversationActivityV2.TAKE_PHOTO, thread); } override fun onAttachmentChanged() { @@ -1006,11 +1013,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val uri = intent?.data ?: return prepMediaForSending(uri, AttachmentManager.MediaType.DOCUMENT).addListener(mediaPreppedListener) } - TAKE_PHOTO -> { - if (resultCode != RESULT_OK) { return } - val uri = attachmentManager.captureUri ?: return - prepMediaForSending(uri, AttachmentManager.MediaType.IMAGE).addListener(mediaPreppedListener) - } PICK_GIF -> { intent ?: return val uri = intent.data ?: return @@ -1019,7 +1021,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val height = intent.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0) prepMediaForSending(uri, type, width, height).addListener(mediaPreppedListener) } - PICK_FROM_LIBRARY -> { + PICK_FROM_LIBRARY, + TAKE_PHOTO -> { intent ?: return val body = intent.getStringExtra(MediaSendActivity.EXTRA_MESSAGE) val media = intent.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA) ?: return @@ -1114,7 +1117,61 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) } - override fun deleteMessages(messages: Set) { + 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) { val messageCount = messages.size val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageDB = DatabaseFactory.getLokiMessageDatabase(this@ConversationActivityV2) @@ -1141,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) } @@ -1156,6 +1213,72 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe builder.show() } + override fun deleteMessages(messages: Set) { + 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) { val builder = AlertDialog.Builder(this) val sessionID = messages.first().individualRecipient.address.toString() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 15de18a60..59e9f4d6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt new file mode 100644 index 000000000..4d84f7c0f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DeleteOptionsBottomSheet.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt index 7cd78cda2..b1283e1a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/AlbumThumbnailView.kt @@ -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(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 -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index 237265775..48772f8ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 70e473784..395d68bf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -92,6 +92,9 @@ object ConversationMenuHelper { inflater.inflate(R.menu.menu_conversation_muted, menu) } else { inflater.inflate(R.menu.menu_conversation_unmuted, menu) + } + + if (thread.isGroupRecipient && !thread.isMuted) { inflater.inflate(R.menu.menu_conversation_notification_settings, menu) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt new file mode 100644 index 000000000..a91473352 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/DeletedMessageView.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 0f6fe2773..d873978ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -78,11 +78,13 @@ class QuoteView : LinearLayout { } val body = quoteViewBodyTextView.text val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth) + val staticLayout = TextUtilities.getIntrinsicLayout(body, quoteViewBodyTextView.paint, maxContentWidth) result += bodyTextViewIntrinsicHeight if (!quoteViewAuthorTextView.isVisible) { - // We want to at least be as high as the cancel button, and no higher than 56 DP (that's - // approximately the height of 3 lines. - return min(max(result, toPx(32, resources)), toPx(56, resources)) + // We want to at least be as high as the cancel button 36DP, and no higher than 3 lines of text. + // Height from intrinsic layout is the height of the text before truncation so we shorten + // proportionally to our max lines setting. + return max(toPx(32, resources) ,min((result / staticLayout.lineCount) * 3, result)) } else { // Because we're showing the author text view, we should have a height of at least 32 DP // anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP @@ -97,7 +99,7 @@ class QuoteView : LinearLayout { // and then center everything inside vertically. This effectively means we're applying padding. // Applying padding the regular way results in a clipping issue though due to a bug in // RelativeLayout. - return getIntrinsicContentHeight(maxContentWidth) + 2 * vPadding + return getIntrinsicContentHeight(maxContentWidth) + (2 * vPadding ) } // endregion diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 8f2410652..38831ed5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index ad1864d05..5e5b1da22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java index d1d7e0b81..dd90b699e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/AttachmentManager.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; -import android.provider.MediaStore; import android.provider.OpenableColumns; import android.text.TextUtils; import android.util.Pair; @@ -34,9 +33,12 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.session.libsignal.utilities.NoExternalStorageException; -import org.thoughtcrime.securesms.giph.ui.GiphyActivity; +import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsignal.utilities.ListenableFuture; import org.session.libsignal.utilities.Log; +import org.session.libsignal.utilities.SettableFuture; +import org.session.libsignal.utilities.guava.Optional; +import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.DocumentSlide; @@ -50,16 +52,8 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; -import org.session.libsignal.utilities.ExternalStorageUtil; -import org.thoughtcrime.securesms.util.FileProviderUtil; import org.thoughtcrime.securesms.util.MediaUtil; -import org.session.libsignal.utilities.guava.Optional; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsignal.utilities.ListenableFuture; -import org.session.libsignal.utilities.SettableFuture; - -import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; @@ -67,8 +61,6 @@ import java.util.List; import network.loki.messenger.R; -import static android.provider.MediaStore.EXTRA_OUTPUT; - public class AttachmentManager { private final static String TAG = AttachmentManager.class.getSimpleName(); @@ -278,25 +270,15 @@ public class AttachmentManager { return captureUri; } - public void capturePhoto(Activity activity, int requestCode) { + public void capturePhoto(Activity activity, int requestCode, Recipient recipient) { Permissions.with(activity) .request(Manifest.permission.CAMERA) .withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_camera_permission_in_order_to_take_photos_but_it_has_been_permanently_denied)) .withRationaleDialog(activity.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera),R.drawable.ic_baseline_photo_camera_24) .onAllGranted(() -> { - try { - File captureFile = File.createTempFile("conversation-capture", ".jpg", ExternalStorageUtil.getImageDir(activity)); - Uri captureUri = FileProviderUtil.getUriFor(context, captureFile); - Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - captureIntent.putExtra(EXTRA_OUTPUT, captureUri); - captureIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { - Log.d(TAG, "captureUri path is " + captureUri.getPath()); - this.captureUri = captureUri; - activity.startActivityForResult(captureIntent, requestCode); - } - } catch (IOException | NoExternalStorageException e) { - throw new RuntimeException("Error creating image capture intent.", e); + Intent captureIntent = MediaSendActivity.buildCameraIntent(activity, recipient); + if (captureIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(captureIntent, requestCode); } }) .execute(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt index b7ced4abb..800ace54c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/TextUtilities.kt @@ -20,6 +20,14 @@ object TextUtilities { return layout.height } + fun getIntrinsicLayout(text: CharSequence, paint: TextPaint, width: Int): StaticLayout { + val builder = StaticLayout.Builder.obtain(text, 0, text.length, paint, width) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLineSpacing(0.0f, 1.0f) + .setIncludePad(false) + return builder.build() + } + fun TextView.getIntersectedModalSpans(event: MotionEvent): List { val xInt = event.rawX.toInt() val yInt = event.rawY.toInt() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index c36c197cb..07513ec32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -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())) + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index 0fcb61d74..a3906d287 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 38e9eeccc..f56792aa7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 81ec17bd0..52642b5d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 42284a4ff..86b523164 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index c148f8ebc..17a836477 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index fe9327a13..950b6d823 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index c9fa010e8..361d1449e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 106528ee2..ccf5c9e77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 3adb9cbda..10e4cb753 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt index f6010bb53..f13c2824d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,9 +15,11 @@ import android.widget.Toast import com.google.android.material.bottomsheet.BottomSheetDialogFragment import kotlinx.android.synthetic.main.fragment_user_details_bottom_sheet.* import network.loki.messenger.R +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.utilities.Address import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.UiModeUtilities @@ -63,11 +66,23 @@ class UserDetailsBottomSheet : BottomSheetDialogFragment() { } nameTextView.text = recipient.name ?: publicKey // Uses the Contact API internally publicKeyTextView.text = publicKey - copyButton.setOnClickListener { + publicKeyTextView.setOnLongClickListener { val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText("Session ID", publicKey) clipboard.setPrimaryClip(clip) Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + true + } + messageButton.setOnClickListener { + val threadId = MessagingModuleConfiguration.shared.storage.getThreadId(recipient) + val intent = Intent( + context, + ConversationActivityV2::class.java + ) + intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address) + intent.putExtra(ConversationActivityV2.THREAD_ID, threadId ?: -1) + startActivity(intent) + dismiss() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java index 04edf1194..68c568bb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/longmessage/LongMessageActivity.java @@ -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); - } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index d2b36a689..3e9fd7905 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -20,19 +20,18 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.MediaTypes; +import org.session.libsession.utilities.Util; +import org.session.libsession.utilities.concurrent.SimpleTask; +import org.session.libsession.utilities.recipients.Recipient; +import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; -import org.session.libsignal.utilities.Log; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.scribbles.ImageEditorFragment; -import org.session.libsession.utilities.Address; -import org.session.libsession.utilities.recipients.Recipient; -import org.session.libsession.utilities.concurrent.SimpleTask; -import org.session.libsession.utilities.Util; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -87,7 +86,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple } /** - * Get an intent to launch the media send flow starting with the picker. + * Get an intent to launch the media send flow starting with the camera. */ public static Intent buildCameraIntent(@NonNull Context context, @NonNull Recipient recipient) { Intent intent = buildGalleryIntent(context, recipient, ""); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index afd4ad74a..d6b126752 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -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()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index e7f8e8e5c..936548ecc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -90,11 +90,11 @@ public class DefaultMessageNotifier implements MessageNotifier { private static final String TAG = DefaultMessageNotifier.class.getSimpleName(); - public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; + public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply"; public static final String LATEST_MESSAGE_ID_TAG = "extra_latest_message_id"; - private static final int FOREGROUND_ID = 313399; - private static final int SUMMARY_NOTIFICATION_ID = 1338; + private static final int FOREGROUND_ID = 313399; + private static final int SUMMARY_NOTIFICATION_ID = 1338; private static final int PENDING_MESSAGES_ID = 1111; private static final String NOTIFICATION_GROUP = "messages"; private static final long MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2); @@ -284,8 +284,9 @@ public class DefaultMessageNotifier implements MessageNotifier { sendMultipleThreadNotification(context, notificationState, signal); } else if (notificationState.getMessageCount() > 0){ sendSingleThreadNotification(context, notificationState, signal, false); + } else { + cancelActiveNotifications(context); } - cancelOrphanedNotifications(context, notificationState); updateBadge(context, notificationState.getMessageCount()); @@ -314,12 +315,12 @@ public class DefaultMessageNotifier implements MessageNotifier { List notifications = notificationState.getNotifications(); Recipient recipient = notifications.get(0).getRecipient(); int notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0)); - String messageIdTag = String.valueOf(notifications.get(0).getId()); + String messageIdTag = String.valueOf(notifications.get(0).getTimestamp()); NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); for (StatusBarNotification notification: notificationManager.getActiveNotifications()) { - - if (messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) { + if ( (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && notification.isAppGroup() == bundled) + && messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) { return; } } @@ -401,6 +402,16 @@ public class DefaultMessageNotifier implements MessageNotifier { builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); builder.setAutoCancel(true); + String messageIdTag = String.valueOf(notifications.get(0).getTimestamp()); + + NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); + for (StatusBarNotification notification: notificationManager.getActiveNotifications()) { + if (notification.getId() == SUMMARY_NOTIFICATION_ID + && messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) { + return; + } + } + long timestamp = notifications.get(0).getTimestamp(); if (timestamp != 0) builder.setWhen(timestamp); @@ -420,6 +431,8 @@ public class DefaultMessageNotifier implements MessageNotifier { MentionUtilities.highlightMentions(notifications.get(0).getText(), notifications.get(0).getThreadId(), context)); } + builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag); + Notification notification = builder.build(); NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build()); Log.i(TAG, "Posted notification. " + notification.toString()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java index 24374ddd0..15b62df3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java @@ -16,8 +16,8 @@ import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.database.SessionContactDatabase; +import org.thoughtcrime.securesms.home.HomeActivity; import java.util.LinkedList; import java.util.List; @@ -72,6 +72,10 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu extend(new NotificationCompat.WearableExtender().addAction(markAllAsReadAction)); } + public void putStringExtra(String key, String value) { + extras.putString(key,value); + } + public void addMessageBody(@NonNull Recipient sender, Recipient threadRecipient, @Nullable CharSequence body) { String displayName = sender.toShortString(); if (threadRecipient.isOpenGroupRecipient()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java index c2c4834cd..59cd5cc0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringMessageManager.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java index 13cdcc6d6..2697a634c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AttachmentUtil.java @@ -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); } diff --git a/app/src/main/res/layout/album_thumbnail_view.xml b/app/src/main/res/layout/album_thumbnail_view.xml index 9c7ed0221..706964a20 100644 --- a/app/src/main/res/layout/album_thumbnail_view.xml +++ b/app/src/main/res/layout/album_thumbnail_view.xml @@ -52,7 +52,7 @@ android:layout_width="@dimen/accent_line_thickness" android:layout_height="match_parent" android:background="@color/accent"/> - + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml b/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml index 985de78a6..4b46e134c 100644 --- a/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml +++ b/app/src/main/res/layout/fragment_user_details_bottom_sheet.xml @@ -111,12 +111,12 @@