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(
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<Address?>(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()
}

View File

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

View File

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

View File

@ -8,6 +8,46 @@
android:layout_height="wrap_content"
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
android:id="@+id/dateBreakTextView"
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_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>