feat: Added an unread marker and search result focus highlighting

This commit is contained in:
Morgan Pretty 2023-06-20 09:52:24 +10:00
parent dce89a0f5f
commit a689e38f33
5 changed files with 73 additions and 26 deletions

View file

@ -299,6 +299,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val adapter = ConversationAdapter( val adapter = ConversationAdapter(
this, this,
cursor, cursor,
storage.getLastSeen(viewModel.threadId),
reverseMessageList, reverseMessageList,
onItemPress = { message, position, view, event -> onItemPress = { message, position, view, event ->
handlePress(message, position, view, event) handlePress(message, position, view, event)
@ -342,7 +343,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val messageToScrollTimestamp = AtomicLong(-1) private val messageToScrollTimestamp = AtomicLong(-1)
private val messageToScrollAuthor = AtomicReference<Address?>(null) private val messageToScrollAuthor = AtomicReference<Address?>(null)
private val firstLoad = AtomicBoolean(true) private val firstLoad = AtomicBoolean(true)
private val forceHighlightNextLoad = AtomicInteger(-1)
private lateinit var reactionDelegate: ConversationReactionDelegate private lateinit var reactionDelegate: ConversationReactionDelegate
private val reactWithAnyEmojiStartPage = -1 private val reactWithAnyEmojiStartPage = -1
@ -426,13 +426,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// transitioning to the activity // transitioning to the activity
weakActivity.get()?.adapter ?: return@launch 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) { withContext(Dispatchers.Main) {
setUpRecyclerView() setUpRecyclerView()
setUpTypingObserver() setUpTypingObserver()
setUpRecipientObserver() setUpRecipientObserver()
getLatestOpenGroupInfoIfNeeded() getLatestOpenGroupInfoIfNeeded()
setUpSearchResultObserver() 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) { if (author != null && messageTimestamp >= 0) {
jumpToMessage(author, messageTimestamp, null) jumpToMessage(author, messageTimestamp, true, null)
} }
else if (firstLoad.getAndSet(false)) { else if (firstLoad.getAndSet(false)) {
// We can't actually just 'shouldHighlight = true' here because any unread messages will scrollToFirstUnreadMessageIfNeeded(true)
// immediately be marked as ready triggering a reload of the cursor
val lastSeenItemPosition = scrollToFirstUnreadMessageIfNeeded(true)
handleRecyclerViewScrolled() handleRecyclerViewScrolled()
if (initialUnreadCount > 0 && lastSeenItemPosition != null) {
forceHighlightNextLoad.set(lastSeenItemPosition)
}
} }
else if (oldCount != newCount) { else if (oldCount != newCount) {
handleRecyclerViewScrolled() 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() updatePlaceholder()
} }
@ -2064,7 +2058,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (result == null) return@Observer if (result == null) return@Observer
if (result.getResults().isNotEmpty()) { if (result.getResults().isNotEmpty()) {
result.getResults()[result.position]?.let { result.getResults()[result.position]?.let {
jumpToMessage(it.messageRecipient.address, it.sentTimestampMs) { jumpToMessage(it.messageRecipient.address, it.sentTimestampMs, true) {
searchViewModel.onMissingResult() } searchViewModel.onMissingResult() }
} }
} }
@ -2101,15 +2095,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
this.searchViewModel.onMoveDown() 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, { SimpleTask.run(lifecycle, {
mmsSmsDb.getMessagePositionInConversation(viewModel.threadId, timestamp, author, reverseMessageList) 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) { if (position >= 0) {
binding?.conversationRecyclerView?.scrollToPosition(position) binding?.conversationRecyclerView?.scrollToPosition(position)
if (highlight) {
runOnUiThread {
highlightViewAtPosition(position)
}
}
} else { } else {
onMessageNotFound?.run() onMessageNotFound?.run()
} }

View file

@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity
class ConversationAdapter( class ConversationAdapter(
context: Context, context: Context,
cursor: Cursor, cursor: Cursor,
private val originalLastSeen: Long,
private val isReversed: Boolean, private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit,
@ -130,6 +131,7 @@ class ConversationAdapter(
searchQuery, searchQuery,
contact, contact,
senderId, senderId,
originalLastSeen,
visibleMessageViewDelegate, visibleMessageViewDelegate,
onAttachmentNeedsDownload onAttachmentNeedsDownload
) )

View file

@ -127,6 +127,7 @@ class VisibleMessageView : LinearLayout {
searchQuery: String?, searchQuery: String?,
contact: Contact?, contact: Contact?,
senderSessionID: String, senderSessionID: String,
originalLastSeen: Long,
delegate: VisibleMessageViewDelegate?, delegate: VisibleMessageViewDelegate?,
onAttachmentNeedsDownload: (Long, Long) -> Unit onAttachmentNeedsDownload: (Long, Long) -> Unit
) { ) {
@ -193,6 +194,8 @@ class VisibleMessageView : LinearLayout {
val contactContext = val contactContext =
if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR if (thread.isOpenGroupRecipient) ContactContext.OPEN_GROUP else ContactContext.REGULAR
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
// Unread marker
binding.unreadMarkerContainer.isVisible = message.timestamp > originalLastSeen && (previous == null || previous.timestamp <= originalLastSeen)
// Date break // Date break
val showDateBreak = isStartOfMessageCluster || snIsSelected val showDateBreak = isStartOfMessageCluster || snIsSelected
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null

View file

@ -8,6 +8,46 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/unreadMarkerContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/small_spacing"
android:visibility="gone"
tools:visibility="visible">
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_marginEnd="@dimen/small_spacing"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/unreadMarker"
android:background="?android:colorAccent" />
<TextView
android:id="@+id/unreadMarker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@string/unread_marker"
android:gravity="center"
android:textColor="?android:colorAccent"
android:textSize="@dimen/small_font_size"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginStart="@dimen/small_spacing"
android:layout_marginEnd="@dimen/medium_spacing"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/unreadMarker"
app:layout_constraintEnd_toEndOf="parent"
android:background="?android:colorAccent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView <TextView
android:id="@+id/dateBreakTextView" android:id="@+id/dateBreakTextView"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -1026,4 +1026,6 @@
<string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string> <string name="activity_conversation_empty_state_note_to_self">You have no messages in Note to Self.</string>
<string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string> <string name="activity_conversation_empty_state_default">You have no messages from <b>%s</b>.\nSend a message to start the conversation!</string>
<string name="unread_marker">Unread Messages</string>
</resources> </resources>