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 3ecb1c11ce..8b9d7340c2 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 @@ -3,37 +3,30 @@ package org.thoughtcrime.securesms.conversation.v2 import android.Manifest import android.animation.FloatEvaluator import android.animation.ValueAnimator -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.DialogInterface -import android.content.Intent +import android.content.* import android.content.res.Resources import android.database.Cursor import android.graphics.Rect import android.graphics.Typeface import android.net.Uri -import android.os.AsyncTask -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper +import android.os.* import android.provider.MediaStore +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.SpannedString import android.text.TextUtils +import android.text.style.StyleSpan import android.util.Pair import android.util.TypedValue -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import android.view.MotionEvent -import android.view.View -import android.view.WindowManager +import android.view.* import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog +import androidx.core.text.set +import androidx.core.text.toSpannable import androidx.core.view.drawToBitmap import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle @@ -79,12 +72,8 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.snode.SnodeAPI -import org.session.libsession.utilities.Address +import org.session.libsession.utilities.* 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.Stub -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.RecipientModifiedListener @@ -117,25 +106,10 @@ import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel -import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager -import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog -import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities -import org.thoughtcrime.securesms.conversation.v2.utilities.ResendMessageUtilities +import org.thoughtcrime.securesms.conversation.v2.utilities.* import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.database.GroupDatabase -import org.thoughtcrime.securesms.database.LokiAPIDatabase -import org.thoughtcrime.securesms.database.LokiMessageDatabase -import org.thoughtcrime.securesms.database.LokiThreadDatabase -import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.MmsSmsDatabase -import org.thoughtcrime.securesms.database.ReactionDatabase -import org.thoughtcrime.securesms.database.RecipientDatabase -import org.thoughtcrime.securesms.database.SessionContactDatabase -import org.thoughtcrime.securesms.database.SmsDatabase -import org.thoughtcrime.securesms.database.Storage -import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.* import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord @@ -148,26 +122,13 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivity -import org.thoughtcrime.securesms.mms.AudioSlide -import org.thoughtcrime.securesms.mms.GifSlide -import org.thoughtcrime.securesms.mms.GlideApp -import org.thoughtcrime.securesms.mms.ImageSlide -import org.thoughtcrime.securesms.mms.MediaConstraints -import org.thoughtcrime.securesms.mms.Slide -import org.thoughtcrime.securesms.mms.SlideDeck -import org.thoughtcrime.securesms.mms.VideoSlide +import org.thoughtcrime.securesms.mms.* import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment -import org.thoughtcrime.securesms.util.ActivityDispatcher -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.MediaUtil -import org.thoughtcrime.securesms.util.SaveAttachmentTask -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.util.* import java.lang.ref.WeakReference -import java.util.Locale +import java.util.* import java.util.concurrent.ExecutionException import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference @@ -401,6 +362,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updateUnreadCountIndicator() updateSubtitle() + updatePlaceholder() setUpBlockedBanner() binding!!.searchBottomBar.setEventListener(this) showOrHideInputIfNeeded() @@ -984,6 +946,37 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updateUnreadCountIndicator() } + private fun updatePlaceholder() { + val recipient = viewModel.recipient + ?: return Log.w("Loki", "recipient was null in placeholder update") + val binding = binding ?: return + val openGroup = viewModel.openGroup + val (textResource, insertParam) = when { + recipient.isLocalNumber -> R.string.activity_conversation_empty_state_note_to_self to null + openGroup != null && !openGroup.canWrite -> R.string.activity_conversation_empty_state_read_only to recipient.toShortString() + else -> R.string.activity_conversation_empty_state_default to recipient.toShortString() + } + val showPlaceholder = adapter.itemCount == 0 + binding.placeholderText.isVisible = showPlaceholder + if (showPlaceholder) { + if (insertParam != null) { + val span = getText(textResource) as SpannedString + val annotations = span.getSpans(0, span.length, StyleSpan::class.java) + val boldSpan = annotations.first() + val spannedParam = insertParam.toSpannable() + spannedParam[0 until spannedParam.length] = StyleSpan(boldSpan.style) + val originalStart = span.getSpanStart(boldSpan) + val originalEnd = span.getSpanEnd(boldSpan) + val newString = SpannableStringBuilder(span) + .replace(originalStart, originalEnd, spannedParam) + binding.placeholderText.text = newString + } else { + binding.placeholderText.setText(textResource) + } + + } + } + private fun showOrHideScrollToBottomButton(show: Boolean = true) { binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0 } 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 b916f4474c..0a8aae2d59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -98,7 +98,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF // TODO: maybe add time here from formation / creation message override fun threadCreated(address: Address, threadId: Long) { - Log.d("Loki-DBG", "creating thread for $address") + Log.d("Loki-DBG", "creating thread for $address\nExecution context:\n${Thread.currentThread().stackTrace.joinToString("\n")}") + val volatile = configFactory.convoVolatile ?: return if (address.isGroup) { val groups = configFactory.userGroups ?: return @@ -136,7 +137,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF } override fun threadDeleted(address: Address, threadId: Long) { - Log.d("Loki-DBG", "deleting thread for $address") + Log.d("Loki-DBG", "deleting thread for $address\nExecution context:\n${Thread.currentThread().stackTrace.joinToString("\n")}") val volatile = configFactory.convoVolatile ?: return if (address.isGroup) { val groups = configFactory.userGroups ?: return @@ -460,11 +461,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF val extracted = convos.all() for (conversation in extracted) { val threadId = when (conversation) { - is Conversation.OneToOne -> getOrCreateThreadIdFor(fromSerialized(conversation.sessionId)) - is Conversation.LegacyGroup -> getOrCreateThreadIdFor("", conversation.groupId,null) - is Conversation.Community -> getOrCreateThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl}.${conversation.baseCommunityInfo.room}") + is Conversation.OneToOne -> getThreadIdFor(conversation.sessionId, null, null, createThread = false) + is Conversation.LegacyGroup -> getThreadIdFor("", conversation.groupId,null, createThread = false) + is Conversation.Community -> getThreadIdFor("",null, "${conversation.baseCommunityInfo.baseUrl}.${conversation.baseCommunityInfo.room}", createThread = false) } - if (threadId >= 0) { + if (threadId != null) { markConversationAsRead(threadId, conversation.lastRead) updateThread(threadId, false) } @@ -981,17 +982,19 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient) } - override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long { + override fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? { val database = DatabaseComponent.get(context).threadDatabase() return if (!openGroupID.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) - database.getThreadIdIfExistsFor(recipient) + database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else if (!groupPublicKey.isNullOrEmpty()) { val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) - database.getOrCreateThreadIdFor(recipient) + if (createThread) database.getOrCreateThreadIdFor(recipient) + else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } else { val recipient = Recipient.from(context, fromSerialized(publicKey), false) - database.getOrCreateThreadIdFor(recipient) + if (createThread) database.getOrCreateThreadIdFor(recipient) + else database.getThreadIdIfExistsFor(recipient).let { if (it == -1L) null else it } } } diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 4ec4666d7f..b1079ca892 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -202,6 +202,19 @@ + + Search GIFs? Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs. Some of your devices are using outdated versions. Syncing may be unreliable until they are updated. + + There are no messages in %s. + You have no messages in Note to Self. + You have no messages from %s.\nSend a message to start the conversation! + diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util index 3817b543f5..057318136c 160000 --- a/libsession-util/libsession-util +++ b/libsession-util/libsession-util @@ -1 +1 @@ -Subproject commit 3817b543f5f862b58afd50c285fbccc26bc82ee0 +Subproject commit 057318136c69f582b7fd78a39ee26686d1d5a3ec diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 67a7adcd00..dbdbb75a46 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -155,7 +155,7 @@ interface StorageProtocol { // Thread fun getOrCreateThreadIdFor(address: Address): Long - fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long + fun getThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?, createThread: Boolean): Long? fun getThreadId(publicKeyOrOpenGroupID: String): Long? fun getThreadId(openGroup: OpenGroup): Long? fun getThreadId(address: Address): Long? diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index ec70a940b9..530cacc832 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -10,6 +10,7 @@ import nl.komponents.kovenant.task import org.session.libsession.database.StorageProtocol import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.ParsedMessage @@ -61,13 +62,29 @@ class BatchMessageReceiveJob( private val OPEN_GROUP_ID_KEY = "open_group_id" } - private fun getThreadId(message: Message, storage: StorageProtocol): Long { + private fun shouldCreateThread(parsedMessage: ParsedMessage): Boolean { + val message = parsedMessage.message + return message is VisibleMessage + || !message.isSenderSelf + // TODO: sort out which messages should create threads: message requests? group creation threads? visible messages? others? calls? + +// || (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) // this was creating threads for self send messages (i.e. synced group creation) +// if (message is ClosedGroupControlMessage && message.kind is ClosedGroupControlMessage.Kind.New) { +// return true +// } +// if (parsedMessage.message.isSenderSelf ) { +// // all the cases where we should add a self send creating the thread, i.e. group invite? visible message? +// } +// !parsedMessage.message.isSenderSelf || parsedMessage.message is VisibleMessage + } + + private fun getThreadId(message: Message, storage: StorageProtocol, shouldCreateThread: Boolean): Long? { val senderOrSync = when (message) { is VisibleMessage -> message.syncTarget ?: message.sender!! is ExpirationTimerUpdate -> message.syncTarget ?: message.sender!! else -> message.sender!! } - return storage.getOrCreateThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID) + return storage.getThreadIdFor(senderOrSync, message.groupPublicKey, openGroupID, createThread = shouldCreateThread) } override suspend fun execute(dispatcherName: String) { @@ -88,9 +105,9 @@ class BatchMessageReceiveJob( try { val (message, proto) = MessageReceiver.parse(data, openGroupMessageServerID, openGroupPublicKey = serverPublicKey) message.serverHash = serverHash - val threadID = getThreadId(message, storage) val parsedParams = ParsedMessage(messageParameters, message, proto) - if (!threadMap.containsKey(threadID)) { + val threadID = getThreadId(message, storage, shouldCreateThread(parsedParams)) + if (threadID != null && !threadMap.containsKey(threadID)) { threadMap[threadID] = mutableListOf(parsedParams) } else { threadMap[threadID]!! += parsedParams diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index d201daa98d..db2a6f5c5a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -11,6 +11,7 @@ abstract class Message { var receivedTimestamp: Long? = null var recipient: String? = null var sender: String? = null + var isSenderSelf: Boolean = false var groupPublicKey: String? = null var openGroupServerMessageID: Long? = null var serverHash: String? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 7cb5b95b11..108021dcf5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -149,6 +149,9 @@ object MessageReceiver { if (!message.isSelfSendValid && (sender == userPublicKey || isUserBlindedSender)) { throw Error.SelfSend } + if (sender == userPublicKey || isUserBlindedSender) { + message.isSenderSelf = true + } // Guard against control messages in open groups if (isOpenGroupMessage && message !is VisibleMessage) { throw Error.InvalidMessage diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index b5cfdd9264..ecaab000bf 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -440,7 +440,7 @@ object MessageSender { } fun sendNonDurably(message: Message, address: Address): Promise { - val threadID = MessagingModuleConfiguration.shared.storage.getOrCreateThreadIdFor(address) + val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) return send(message, destination) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index aa882c6771..a075ff42f1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -232,11 +232,9 @@ fun MessageReceiver.handleVisibleMessage( // Get or create thread // FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet // exist. This is intentional, but it's very non-obvious. - val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID) - if (threadID < 0) { + val threadID = storage.getThreadIdFor(message.syncTarget ?: messageSender!!, message.groupPublicKey, openGroupID, createThread = true) // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread - throw MessageReceiver.Error.NoThread - } + ?: throw MessageReceiver.Error.NoThread val threadRecipient = storage.getRecipientForThread(threadID) val userBlindedKey = openGroupID?.let { val openGroup = storage.getOpenGroup(threadID) ?: return@let null