From 93a2907fce903a5af0628b1ea0364b97ed62b67f Mon Sep 17 00:00:00 2001 From: Naveen Singh <36371707+naveensingh@users.noreply.github.com> Date: Wed, 15 Oct 2025 03:15:37 +0530 Subject: [PATCH] fix: fetch older messages as needed before jumping to searched message (#557) Refs: https://github.com/FossifyOrg/Messages/issues/350 --- CHANGELOG.md | 4 +- app/detekt-baseline.xml | 39 +----- app/lint-baseline.xml | 124 +++++++++++------- .../messages/activities/ThreadActivity.kt | 87 ++++++++---- .../messages/adapters/ThreadAdapter.kt | 12 +- 5 files changed, 149 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 767ec6d6..2c4e9280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed position reset when opening attachments in conversations ([#82]) +- Fixed automatic scroll to searched message in conversations ([#350]) ## [1.4.0] - 2025-10-12 ### Added @@ -148,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#13]: https://github.com/FossifyOrg/Messages/issues/13 [#70]: https://github.com/FossifyOrg/Messages/issues/70 [#75]: https://github.com/FossifyOrg/Messages/issues/75 +[#82]: https://github.com/FossifyOrg/Messages/issues/82 [#99]: https://github.com/FossifyOrg/Messages/issues/99 [#115]: https://github.com/FossifyOrg/Messages/issues/115 [#135]: https://github.com/FossifyOrg/Messages/issues/135 @@ -167,8 +169,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#309]: https://github.com/FossifyOrg/Messages/issues/309 [#334]: https://github.com/FossifyOrg/Messages/issues/334 [#349]: https://github.com/FossifyOrg/Messages/issues/349 +[#350]: https://github.com/FossifyOrg/Messages/issues/350 [#359]: https://github.com/FossifyOrg/Messages/issues/359 -[#82]: https://github.com/FossifyOrg/Messages/issues/82 [#456]: https://github.com/FossifyOrg/Messages/issues/456 [#461]: https://github.com/FossifyOrg/Messages/issues/461 diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 515a9b6d..c68cc930 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -12,7 +12,7 @@ CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$@SuppressLint("MissingPermission") private fun getThreadItems(): ArrayList<ThreadItem> CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun refreshMenuItems() CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupButtons() - CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupThread() + CyclomaticComplexMethod:ThreadActivity.kt$ThreadActivity$private fun setupThread(callback: () -> Unit) EmptyCatchBlock:MessagesWriter.kt$MessagesWriter${ } EmptyFunctionBlock:ArchivedConversationsAdapter.kt$ArchivedConversationsAdapter${} EmptyFunctionBlock:BaseConversationsAdapter.kt$BaseConversationsAdapter${} @@ -20,7 +20,6 @@ EmptyFunctionBlock:ManageBlockedKeywordsAdapter.kt$ManageBlockedKeywordsAdapter${} EmptyFunctionBlock:RecycleBinConversationsAdapter.kt$RecycleBinConversationsAdapter${} EmptyFunctionBlock:SearchResultsAdapter.kt$SearchResultsAdapter${} - EmptyFunctionBlock:ThreadActivity.kt$ThreadActivity.<no name provided>${} EmptyFunctionBlock:ThreadAdapter.kt$ThreadAdapter${} ForbiddenComment:MainActivity.kt$MainActivity$// FIXME: Scheduled message date is being reset here. Conversations with ForbiddenComment:ShortcutHelper.kt$ShortcutHelper$// TODO: verify that thread isn't in recycle bin @@ -48,7 +47,6 @@ MagicNumber:ImageCompressor.kt$ImageCompressor$8 MagicNumber:ImageCompressor.kt$ImageCompressor$90f MagicNumber:MainActivity.kt$MainActivity$30 - MagicNumber:Message.kt$Message.Companion$31 MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$3 MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$4 MagicNumber:MessagesDatabase.kt$MessagesDatabase.Companion.<no name provided>$5 @@ -76,14 +74,9 @@ MagicNumber:SmsStatusDeliveredReceiver.kt$SmsStatusDeliveredReceiver$3 MagicNumber:ThreadActivity.kt$ThreadActivity$0.4f MagicNumber:ThreadActivity.kt$ThreadActivity$0.9f - MagicNumber:ThreadActivity.kt$ThreadActivity$14 - MagicNumber:ThreadActivity.kt$ThreadActivity$15 MagicNumber:ThreadActivity.kt$ThreadActivity$150 - MagicNumber:ThreadActivity.kt$ThreadActivity$16 MagicNumber:ThreadActivity.kt$ThreadActivity$2 - MagicNumber:ThreadActivity.kt$ThreadActivity$20 MagicNumber:ThreadActivity.kt$ThreadActivity$30 - MagicNumber:ThreadActivity.kt$ThreadActivity$300 MagicNumber:ThreadActivity.kt$ThreadActivity$500L MagicNumber:ThreadAdapter.kt$ThreadAdapter$0.8f MagicNumber:ThreadAdapter.kt$ThreadAdapter$4 @@ -103,8 +96,6 @@ MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE (SELECT COUNT(*) FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NOT NULL AND messages.thread_id = conversations.thread_id) > 0") MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE archived = 0") MaxLineLength:ConversationsDao.kt$ConversationsDao$@Query("SELECT (SELECT body FROM messages LEFT OUTER JOIN recycle_bin_messages ON messages.id = recycle_bin_messages.id WHERE recycle_bin_messages.id IS NULL AND messages.thread_id = conversations.thread_id ORDER BY messages.date DESC LIMIT 1) as new_snippet, * FROM conversations WHERE archived = 1") - MaxLineLength:DirectReplyReceiver.kt$DirectReplyReceiver$context.notificationHelper.showMessageNotification(messageId, address, body, threadId, bitmap, sender = null, alertOnlyOnce = true) - MaxLineLength:DirectReplyReceiver.kt$DirectReplyReceiver$val message = context.getMessages(threadId, getImageResolutions = false, includeScheduledMessages = false, limit = 1).lastOrNull() MaxLineLength:ExportBlockedKeywordsDialog.kt$ExportBlockedKeywordsDialog$exportBlockedKeywordsFilename.setText("${activity.getString(R.string.blocked_keywords)}_${activity.getCurrentFormattedDateTime()}") MaxLineLength:Gson.kt$private val gsonBuilder = GsonBuilder().registerTypeAdapter(object : TypeToken<Map<String, Any>>() {}.type, MapDeserializerDoubleAsIntFix()) MaxLineLength:HeadlessSmsSendService.kt$HeadlessSmsSendService$val number = Uri.decode(intent.dataString!!.removePrefix("sms:").removePrefix("smsto:").removePrefix("mms").removePrefix("mmsto:").trim()) @@ -150,7 +141,6 @@ MaxLineLength:ThreadAdapter.kt$ThreadAdapter$mimetype.isImageMimeType() || mimetype.isVideoMimeType() -> setupImageView(holder, binding = this, message, attachment) MaxLineLength:ThreadAdapter.kt$ThreadAdapter$mimetype.isVCardMimeType() -> setupVCardView(holder, threadMessageAttachmentsHolder, message, attachment) MaxLineLength:ThreadAdapter.kt$ThreadAdapter$private - MaxLineLength:ThreadAdapter.kt$ThreadAdapter$private fun getSelectedItems() MaxLineLength:ThreadAdapter.kt$ThreadAdapter$threadSuccess.setImageResource(if (isDelivered) R.drawable.ic_check_double_vector else org.fossify.commons.R.drawable.ic_check_vector) MaxLineLength:ThreadAdapter.kt$ThreadAdapter$val MaxLineLength:ThreadAdapter.kt$ThreadAdapter.<no name provided>$override @@ -169,23 +159,13 @@ NestedBlockDepth:MessagingUtils.kt$MessagingUtils$@Deprecated("TODO: Move/rewrite MMS code into the app.") fun sendMmsMessage( text: String, addresses: List<String>, attachment: Attachment?, settings: Settings, messageId: Long? = null ) NestedBlockDepth:MessagingUtils.kt$MessagingUtils$fun updateSmsMessageSendingStatus(messageUri: Uri?, type: Int) NestedBlockDepth:SmsStatusDeliveredReceiver.kt$SmsStatusDeliveredReceiver$override fun updateAndroidDatabase(context: Context, intent: Intent, receiverResultCode: Int) - NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun fetchNextMessages() - NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun setupAttachmentSizes() NestedBlockDepth:ThreadActivity.kt$ThreadActivity$private fun setupButtons() NestedBlockDepth:ThreadAdapter.kt$ThreadAdapter$private fun setupSentMessageView(messageBinding: ItemMessageBinding, message: Message) NestedBlockDepth:ThreadAdapter.kt$ThreadAdapter$private fun setupView(holder: ViewHolder, view: View, message: Message) PrintStackTrace:Context.kt$e PrintStackTrace:ScheduledMessageReceiver.kt$ScheduledMessageReceiver$e PrintStackTrace:SmsManager.kt$e - ReturnCount:Context.kt$fun Context.getFileSizeFromUri(uri: Uri): Long - ReturnCount:Context.kt$fun Context.getNameAndPhotoFromPhoneNumber(number: String): NamePhoto ReturnCount:MapDeserializerDoubleAsIntFix.kt$MapDeserializerDoubleAsIntFix$fun read(element: JsonElement): Any? - ReturnCount:MessagesReader.kt$MessagesReader$@SuppressLint("NewApi") private fun usePart(partId: Long, block: (InputStream) -> String): String - ReturnCount:ShortcutHelper.kt$ShortcutHelper$fun shouldPresentShortcut(conv: Conversation): Boolean - ReturnCount:SmsIntentParser.kt$SmsIntentParser$private fun extractBodyFromUri(uri: Uri?): String? - ReturnCount:SmsIntentParser.kt$SmsIntentParser$private fun parseRecipientsFromUri(uri: Uri?): Array<String>? - ReturnCount:ThreadActivity.kt$ThreadActivity$private fun addAttachment(uri: Uri) - ReturnCount:VCardParser.kt$fun VCard?.parseNameFromVCard(): String? SpreadOperator:Context.kt$(*scheduledMessages) SpreadOperator:MainActivity.kt$MainActivity$(*currentMessages.toTypedArray()) SpreadOperator:ThreadActivity.kt$ThreadActivity$(*currentMessages.toTypedArray()) @@ -256,21 +236,11 @@ TooManyFunctions:ShortcutHelper.kt$ShortcutHelper TooManyFunctions:ThreadActivity.kt$ThreadActivity : SimpleActivity TooManyFunctions:ThreadAdapter.kt$ThreadAdapter : MyRecyclerViewListAdapter - UnusedParameter:ArchivedConversationsActivity.kt$ArchivedConversationsActivity$event: Events.RefreshMessages - UnusedParameter:MainActivity.kt$MainActivity$event: Events.RefreshMessages - UnusedParameter:RecycleBinConversationsActivity.kt$RecycleBinConversationsActivity$event: Events.RefreshMessages - UnusedParameter:ThreadActivity.kt$ThreadActivity$event: Events.RefreshMessages UseCheckOrError:AttachmentUtils.kt$AttachmentUtils$throw IllegalStateException() UseCheckOrError:MessagesImporter.kt$MessagesImporter$throw IllegalStateException() UseRequire:SmsSender.kt$SmsSender$throw IllegalArgumentException("SmsSender: empty text message") VariableNaming:MainActivity.kt$MainActivity$private val MAKE_DEFAULT_APP_REQUEST = 1 VariableNaming:MessagesWriter.kt$MessagesWriter$private val INVALID_ID = -1L - VariableNaming:ThreadActivity.kt$ThreadActivity$private val MIN_DATE_TIME_DIFF_SECS = 300 - VariableNaming:ThreadActivity.kt$ThreadActivity$private val SCROLL_TO_BOTTOM_FAB_LIMIT = 20 - VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_DELETE = 16 - VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_EDIT = 14 - VariableNaming:ThreadActivity.kt$ThreadActivity$private val TYPE_SEND = 15 - WildcardImport:ArchivedConversationsActivity.kt$import org.fossify.commons.extensions.* WildcardImport:AttachmentPreviews.kt$import org.fossify.commons.extensions.* WildcardImport:AttachmentPreviews.kt$import org.fossify.messages.extensions.* WildcardImport:AttachmentsAdapter.kt$import org.fossify.commons.extensions.* @@ -280,13 +250,6 @@ WildcardImport:JsonElement.kt$import com.google.gson.* WildcardImport:ManageBlockedKeywordsAdapter.kt$import android.view.* WildcardImport:MessagesDao.kt$import androidx.room.* - WildcardImport:SmsReceiver.kt$import org.fossify.messages.extensions.* - WildcardImport:SmsStatusSentReceiver.kt$import org.fossify.messages.extensions.* - WildcardImport:ThreadAdapter.kt$import org.fossify.commons.extensions.* - WildcardImport:ThreadAdapter.kt$import org.fossify.messages.databinding.* - WildcardImport:ThreadAdapter.kt$import org.fossify.messages.extensions.* - WildcardImport:ThreadAdapter.kt$import org.fossify.messages.helpers.* - WildcardImport:ThreadAdapter.kt$import org.fossify.messages.models.ThreadItem.* WildcardImport:VCard.kt$import ezvcard.property.* WildcardImport:VCardViewerAdapter.kt$import org.fossify.commons.extensions.* diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index 5c5de559..2faad604 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -29,7 +29,7 @@ errorLine1="app-build-targetSDK = "34"" errorLine2=" ~~~~"> @@ -51,7 +51,7 @@ errorLine1="distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip" errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -62,7 +62,7 @@ errorLine1="gradlePlugins-agp = "8.11.1"" errorLine2=" ~~~~~~~~"> @@ -73,7 +73,7 @@ errorLine1="androidx-lifecycleprocess = "2.8.7"" errorLine2=" ~~~~~~~"> @@ -84,7 +84,7 @@ errorLine1="app-build-compileSDKVersion = "34"" errorLine2=" ~~~~"> @@ -212,7 +212,7 @@ + + + + + + + + + + + + @@ -2149,21 +2182,10 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> - - - - Unit) { if (conversation == null && isLaunchedFromShortcut) { if (isTaskRoot) { Intent(this, MainActivity::class.java).apply { @@ -494,6 +494,7 @@ class ThreadActivity : SimpleActivity() { try { if (participants.isNotEmpty() && messages.hashCode() == cachedMessagesCode && !hasParticipantWithoutName) { setupAdapter() + runOnUiThread { callback() } return@ensureBackgroundThread } } catch (ignored: Exception) { @@ -553,6 +554,7 @@ class ThreadActivity : SimpleActivity() { runOnUiThread { setupThreadTitle() setupSIMSelector() + callback() } } } @@ -717,7 +719,44 @@ class ThreadActivity : SimpleActivity() { } } + private fun jumpToMessage(messageId: Long) { + if (messages.any { it.id == messageId }) { + val index = threadItems.indexOfFirst { (it as? Message)?.id == messageId } + if (index != -1) binding.threadMessagesList.smoothScrollToPosition(index) + return + } + + ensureBackgroundThread { + if (loadingOlderMessages) return@ensureBackgroundThread + loadingOlderMessages = true + isJumpingToMessage = true + + var cutoff = messages.firstOrNull()?.date ?: Int.MAX_VALUE + var found = false + var loops = 0 + + // not the best solution, but this will do for now. + while (!found && !allMessagesFetched) { + if (fetchOlderMessages(cutoff).isEmpty() || loops >= 1000) break + cutoff = messages.first().date + found = messages.any { it.id == messageId } + loops++ + } + + threadItems = getThreadItems() + runOnUiThread { + loadingOlderMessages = false + val index = threadItems.indexOfFirst { (it as? Message)?.id == messageId } + getOrCreateThreadAdapter().updateMessages( + newMessages = threadItems, scrollPosition = index, smoothScroll = true + ) + isJumpingToMessage = false + } + } + } + private fun tryLoadMoreMessages() { + if (isJumpingToMessage) return val layoutManager = binding.threadMessagesList.layoutManager as LinearLayoutManager if (layoutManager.findFirstVisibleItemPosition() <= PREFETCH_THRESHOLD) { loadMoreMessages() @@ -726,21 +765,11 @@ class ThreadActivity : SimpleActivity() { private fun loadMoreMessages() { if (messages.isEmpty() || allMessagesFetched || loadingOlderMessages) return - - val firstItem = messages.first() - val dateOfFirstItem = firstItem.date - - oldestMessageDate = dateOfFirstItem loadingOlderMessages = true - + val cutoff = messages.first().date ensureBackgroundThread { - val olderMessages = getMessages(threadId, oldestMessageDate) - .filterNotInByKey(messages) { it.getStableId() } - - messages.addAll(0, olderMessages) - allMessagesFetched = olderMessages.isEmpty() + fetchOlderMessages(cutoff) threadItems = getThreadItems() - runOnUiThread { loadingOlderMessages = false getOrCreateThreadAdapter().updateMessages(threadItems) @@ -748,23 +777,32 @@ class ThreadActivity : SimpleActivity() { } } + private fun fetchOlderMessages(cutoff: Int): List { + val older = getMessages(threadId, cutoff) + .filterNotInByKey(messages) { it.getStableId() } + + if (older.isEmpty()) { + allMessagesFetched = true + return older + } + + messages.addAll(0, older) + return older + } + private fun loadConversation() { handlePermission(PERMISSION_READ_PHONE_STATE) { granted -> if (granted) { setupButtons() setupConversation() setupCachedMessages { - val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L) - intent.removeExtra(SEARCHED_MESSAGE_ID) - if (searchedMessageId != -1L) { - val index = - threadItems.indexOfFirst { (it as? Message)?.id == searchedMessageId } - if (index != -1) { - binding.threadMessagesList.smoothScrollToPosition(index) + setupThread { + val searchedMessageId = intent.getLongExtra(SEARCHED_MESSAGE_ID, -1L) + intent.removeExtra(SEARCHED_MESSAGE_ID) + if (searchedMessageId != -1L) { + jumpToMessage(searchedMessageId) } } - - setupThread() setupScrollListener() } } else { @@ -1712,14 +1750,13 @@ class ThreadActivity : SimpleActivity() { } @Subscribe(threadMode = ThreadMode.ASYNC) - fun refreshMessages(event: Events.RefreshMessages) { + fun refreshMessages(@Suppress("unused") event: Events.RefreshMessages) { if (isRecycleBin) { return } refreshedSinceSent = true allMessagesFetched = false - oldestMessageDate = -1 if (isActivityVisible) { notificationManager.cancel(threadId.hashCode()) diff --git a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt index 86d13fbe..a3a8518b 100644 --- a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt @@ -328,11 +328,19 @@ class ThreadAdapter( private fun isThreadDateTime(position: Int) = currentList.getOrNull(position) is ThreadDateTime - fun updateMessages(newMessages: ArrayList, scrollPosition: Int = -1) { + fun updateMessages( + newMessages: ArrayList, + scrollPosition: Int = -1, + smoothScroll: Boolean = false + ) { val latestMessages = newMessages.toMutableList() submitList(latestMessages) { if (scrollPosition != -1) { - recyclerView.scrollToPosition(scrollPosition) + if (smoothScroll) { + recyclerView.smoothScrollToPosition(scrollPosition) + } else { + recyclerView.scrollToPosition(scrollPosition) + } } } }