From a689e38f337a66a4b7400ab994ef61404e8eb3e7 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 20 Jun 2023 09:52:24 +1000 Subject: [PATCH] feat: Added an unread marker and search result focus highlighting --- .../conversation/v2/ConversationActivityV2.kt | 52 +++++++++---------- .../conversation/v2/ConversationAdapter.kt | 2 + .../v2/messages/VisibleMessageView.kt | 3 ++ .../main/res/layout/view_visible_message.xml | 40 ++++++++++++++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 73 insertions(+), 26 deletions(-) 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 8048c6abec..d953aa7330 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 @@ -299,6 +299,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val adapter = ConversationAdapter( this, cursor, + storage.getLastSeen(viewModel.threadId), reverseMessageList, onItemPress = { message, position, view, event -> handlePress(message, position, view, event) @@ -342,7 +343,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val messageToScrollTimestamp = AtomicLong(-1) private val messageToScrollAuthor = AtomicReference(null) private val firstLoad = AtomicBoolean(true) - private val forceHighlightNextLoad = AtomicInteger(-1) private lateinit var reactionDelegate: ConversationReactionDelegate private val reactWithAnyEmojiStartPage = -1 @@ -426,13 +426,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // transitioning to the activity weakActivity.get()?.adapter ?: return@launch + // 'Get' instead of 'GetAndSet' here because we want to trigger the highlight in 'onFirstLoad' + // by triggering 'jumpToMessage' using these values + val messageTimestamp = messageToScrollTimestamp.get() + val author = messageToScrollAuthor.get() + val targetPosition = if (author != null && messageTimestamp >= 0) mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, messageTimestamp, author, reverseMessageList) else -1 + withContext(Dispatchers.Main) { setUpRecyclerView() setUpTypingObserver() setUpRecipientObserver() getLatestOpenGroupInfoIfNeeded() setUpSearchResultObserver() - scrollToFirstUnreadMessageIfNeeded(true) + + if (author != null && messageTimestamp >= 0 && targetPosition >= 0) { + binding?.conversationRecyclerView?.scrollToPosition(targetPosition) + } + else { + scrollToFirstUnreadMessageIfNeeded(true) + } } } @@ -514,33 +526,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } if (author != null && messageTimestamp >= 0) { - jumpToMessage(author, messageTimestamp, null) + jumpToMessage(author, messageTimestamp, true, null) } else if (firstLoad.getAndSet(false)) { - // We can't actually just 'shouldHighlight = true' here because any unread messages will - // immediately be marked as ready triggering a reload of the cursor - val lastSeenItemPosition = scrollToFirstUnreadMessageIfNeeded(true) + scrollToFirstUnreadMessageIfNeeded(true) handleRecyclerViewScrolled() - - if (initialUnreadCount > 0 && lastSeenItemPosition != null) { - forceHighlightNextLoad.set(lastSeenItemPosition) - } } else if (oldCount != newCount) { handleRecyclerViewScrolled() } - else { - // Really annoying but if a message gets marked as read during the initial load it'll - // immediately result in a subsequent load of the cursor, if we trigger the highlight - // within the 'firstLoad' it generally ends up getting repositioned as the views get - // recycled and the wrong view is highlighted - by doing it on the subsequent load the - // correct view is highlighted - val forceHighlightPosition = forceHighlightNextLoad.getAndSet(-1) - - if (forceHighlightPosition != -1) { - highlightViewAtPosition(forceHighlightPosition) - } - } } updatePlaceholder() } @@ -2064,7 +2058,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (result == null) return@Observer if (result.getResults().isNotEmpty()) { result.getResults()[result.position]?.let { - jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) { + jumpToMessage(it.messageRecipient.address, it.sentTimestampMs, true) { searchViewModel.onMissingResult() } } } @@ -2101,15 +2095,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe this.searchViewModel.onMoveDown() } - private fun jumpToMessage(author: Address, timestamp: Long, onMessageNotFound: Runnable?) { + private fun jumpToMessage(author: Address, timestamp: Long, highlight: Boolean, onMessageNotFound: Runnable?) { SimpleTask.run(lifecycle, { mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author, reverseMessageList) - }) { p: Int -> moveToMessagePosition(p, onMessageNotFound) } + }) { p: Int -> moveToMessagePosition(p, highlight, onMessageNotFound) } } - private fun moveToMessagePosition(position: Int, onMessageNotFound: Runnable?) { + private fun moveToMessagePosition(position: Int, highlight: Boolean, onMessageNotFound: Runnable?) { if (position >= 0) { binding?.conversationRecyclerView?.scrollToPosition(position) + + if (highlight) { + runOnUiThread { + highlightViewAtPosition(position) + } + } } else { onMessageNotFound?.run() } 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 2b5e4adc15..e57be95fb0 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 @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity class ConversationAdapter( context: Context, cursor: Cursor, + private val originalLastSeen: Long, private val isReversed: Boolean, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, @@ -130,6 +131,7 @@ class ConversationAdapter( searchQuery, contact, senderId, + originalLastSeen, visibleMessageViewDelegate, onAttachmentNeedsDownload ) 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 0befff3163..9bd7dacb65 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 @@ -127,6 +127,7 @@ class VisibleMessageView : LinearLayout { searchQuery: String?, contact: Contact?, senderSessionID: String, + originalLastSeen: Long, delegate: VisibleMessageViewDelegate?, onAttachmentNeedsDownload: (Long, Long) -> Unit ) { @@ -193,6 +194,8 @@ class VisibleMessageView : LinearLayout { val contactContext = if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID + // Unread marker + binding.unreadMarkerContainer.isVisible = message.timestamp > originalLastSeen && (previous == null || previous.timestamp <= originalLastSeen) // Date break val showDateBreak = isStartOfMessageCluster || snIsSelected binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index d328d19ab3..11155ccc20 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -8,6 +8,46 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + + + + + You have no messages in Note to Self. You have no messages from %s.\nSend a message to start the conversation! + Unread Messages +